Skip to content

feat(security): add per-plugin capability permissions and pre-execution enforcement#368

Merged
utksh1 merged 11 commits into
utksh1:mainfrom
advikdivekar:security/per-plugin-capability-permissions
Jun 1, 2026
Merged

feat(security): add per-plugin capability permissions and pre-execution enforcement#368
utksh1 merged 11 commits into
utksh1:mainfrom
advikdivekar:security/per-plugin-capability-permissions

Conversation

@advikdivekar
Copy link
Copy Markdown
Contributor

@advikdivekar advikdivekar commented May 28, 2026

What is the problem
SecuScan had no mechanism to restrict which capabilities individual plugins could exercise at runtime. An operator running in an air-gapped or restricted environment had no way to prevent plugins from making network calls, accessing secrets, or running exploits. The safety.level field existed but was purely advisory with no enforcement boundary. Closes #205.

What was changed

File Change
backend/secuscan/capabilities.py New module: Capability enum (network, filesystem, docker, credentials, intrusive, exploit), CapabilityEnforcer, CapabilityDeniedError, validate_capability_list, effective_capabilities, build_enforcer_from_settings
backend/secuscan/models.py Added optional capabilities: List[str] field to PluginMetadata
backend/secuscan/config.py Added denied_capabilities: List[str] driven by SECUSCAN_DENIED_CAPABILITIES env var
backend/secuscan/executor.py Imports CapabilityEnforcer at startup; calls enforcer.check() before build_command(); catches CapabilityDeniedError with its own audit log event and persists the task as failed
backend/secuscan/plugins.py Validates declared capabilities against the known set at plugin load time
plugins/waf_detector/metadata.json Representative example: added "capabilities": ["network"]
testing/backend/unit/test_capabilities.py 36 unit tests: allow, deny, partial denial, legacy fallback, normalisation, error metadata, settings integration
.env.example Documents SECUSCAN_DENIED_CAPABILITIES with examples

Backward compatibility — explicit documentation

Plugins that do not declare a capabilities field are not broken. An implied capability set is derived from their existing safety.level field:

safety.level Implied capabilities
safe ["network"]
intrusive ["network", "intrusive"]
exploit ["network", "intrusive", "exploit"]

This means:

  • All 60+ existing plugins continue to load and execute without modification
  • Operators can immediately deny capabilities (e.g. SECUSCAN_DENIED_CAPABILITIES=exploit) and all exploit-level plugins are blocked, even without explicit capabilities declarations
  • Plugin authors can optionally add an explicit capabilities list for fine-grained visibility — only one representative example (waf_detector) is updated in this PR to show the pattern

Why this approach
The check runs inside execute_task immediately before build_command(), so no subprocess is ever spawned for a denied plugin. The enforcer is constructed once at executor startup from settings, keeping the hot path allocation-free.

How to test

  1. SECUSCAN_DENIED_CAPABILITIES=exploit python -m secuscan
  2. Submit a task for any exploit-level plugin (e.g. sqlmap, zap_scanner)
  3. Task immediately transitions to failed with task_capability_denied audit log entry
  4. Submit a task for nmap (safe plugin) — runs normally
  5. python -m pytest testing/backend/unit/test_capabilities.py -v — 36 tests pass

Edge cases covered

  • Plugin without capabilities field falls back to safety-level implied set (backward compatible)
  • Partial denial: only overlapping capabilities trigger a block
  • Capability tokens normalised (trimmed, lowercased)
  • Empty strings and whitespace-only entries in the denied list are ignored
  • Unknown capability tokens in plugin metadata fail plugin load, not task execution
  • CapabilityDeniedError carries plugin_id and denied_capabilities for structured logging
  • Audit log event distinguishes capability denials from other task failures

Lines changed
824 insertions, 122 deletions — 585 backend tests pass, 0 failures

Verification

  • Root cause fully resolved — capability enforcement active for all plugins via safety-level fallback
  • Backward compatibility: all existing plugins load and run without schema changes
  • Only 1 plugin metadata file changed (waf_detector as representative example)
  • 36 capability unit tests pass covering allow/deny paths
  • 585 total backend tests pass — no regressions
  • Rebased onto latest main — no merge conflicts
  • Code matches project style and conventions throughout

Labels: type:security type:feature level:advanced gssoc:approved

Closes #205

Please review and merge this under GSSoC 2026.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6594c5459e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/nmap/metadata.json Outdated
Comment on lines +206 to +208
"capabilities": [
"network"
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate plugin checksums after metadata edits

Adding capabilities changes the canonical metadata digest used by PluginManager.compute_plugin_digest, but the existing checksum value is left unchanged. Because _verify_plugin_integrity rejects any plugin with a mismatched checksum even when signature enforcement is off, every plugin whose metadata was edited here is skipped during load_plugins; I recomputed the digest for this file and it no longer matches the checked-in checksum, so the Nmap plugin will disappear from the API instead of gaining capability enforcement.

Useful? React with 👍 / 👎.

plugin_signature_key: Optional[str] = None
enforce_plugin_signatures: bool = False
vault_key: Optional[str] = None
denied_capabilities: List[str] = []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse denied capabilities as CSV from the environment

This new List[str] setting is documented in .env.example as SECUSCAN_DENIED_CAPABILITIES=exploit,credentials, but it is not included in the existing CSV parser validator used for other comma-separated list settings. In deployments that follow the documented example, pydantic-settings treats list environment values as JSON unless custom parsing is applied, so the server will either fail settings loading or never produce the intended ['exploit', 'credentials'] denied set; include this field in parse_csv_or_list (or use equivalent custom parsing) so the advertised policy knob works.

Useful? React with 👍 / 👎.

@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review all my PRs, people have started to raise duplicate issues and are merging it but mine is not getting merged, please review into this matter, thank you

@utksh1 utksh1 added area:backend Backend API, database, or service work area:plugins Scanner plugin metadata, schemas, or plugin runtime work area:security Security-sensitive implementation or tests type:security Security work category bonus label type:feature Feature work category bonus label type:testing Testing work category bonus label level:critical 80 pts difficulty label for critical or high-impact PRs labels May 28, 2026
Copy link
Copy Markdown
Owner

@utksh1 utksh1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. Capability permissions are a good direction, but backend-tests are failing and the PR changes metadata across dozens of plugins in one shot. Please first get CI green, then narrow the PR to the core enforcement model plus a small representative set of plugin metadata and tests. Also document the migration/backward-compatibility behavior for plugins without capability declarations.

@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review this pr, it is ready to merge, let me know if any improvements required otherwise it's good to review and get merged, thank you for assigning me this issue

Copy link
Copy Markdown
Owner

@utksh1 utksh1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed latest state. The capability model is useful, but the PR still changes too much metadata surface at once and needs a clearer backward-compatibility story for plugins without declarations. Please narrow to core enforcement plus representative plugin metadata, document migration behavior, and keep tests focused on allow/deny paths.

Plugins declare a capabilities list in metadata.json (network, filesystem,
docker, credentials, intrusive, exploit).  CapabilityEnforcer checks those
declarations against the operator-configured SECUSCAN_DENIED_CAPABILITIES
list before any command is built or subprocess is spawned.

Legacy plugins without an explicit capabilities field fall back to an implied
set derived from their safety level so enforcement works across the full
plugin catalogue without a hard cut-over.

- capabilities.py: Capability enum, CapabilityEnforcer, CapabilityDeniedError,
  validate_capability_list, effective_capabilities, build_enforcer_from_settings
- models.py: optional capabilities field on PluginMetadata
- config.py: denied_capabilities setting (SECUSCAN_DENIED_CAPABILITIES env var)
- executor.py: capability check before command build; dedicated audit log event
  on denial; CapabilityDeniedError caught and persisted as task failure
- plugins.py: validate capabilities at load time; expose in list_plugins response
- All 60 plugin metadata.json files: explicit capabilities declarations
- test_capabilities.py: 36 unit tests covering allow, deny, normalisation,
  legacy fallback, error metadata, and settings integration
- .env.example: documented SECUSCAN_DENIED_CAPABILITIES with examples

Closes utksh1#205
…e, document backward compat

Revert all 60 plugin metadata.json changes: adding capabilities to every
plugin file invalidated their checksums (capabilities is included in the
digest computation), causing all plugins to fail to load and collapsing
the test suite.  Enforcement works for all existing plugins via the
safety-level implied-capability fallback, so no metadata changes are
needed for the feature to be active.

Fix duplicate plugin display name: both waf-detection and waf_detector
had the name "WAF Detector"; rename waf_detector to "WAF Detector (wafw00f)"
and refresh its checksum.

Add explicit backward-compatibility documentation to capabilities.py
explaining the implied-set fallback and the checksum-refresh requirement
for plugin authors who opt in to explicit capability declarations.
@advikdivekar advikdivekar force-pushed the security/per-plugin-capability-permissions branch from 1788481 to 1081c34 Compare May 30, 2026 06:58
@utksh1
Copy link
Copy Markdown
Owner

utksh1 commented May 30, 2026

Re-reviewed after the latest push. Still blocked: please narrow this to core capability enforcement plus representative plugin metadata, document backward-compatibility behavior for plugins without declarations, and keep tests focused on allow/deny paths.

…ctor

Adds capabilities: ["network"] as a representative example showing how
plugins should declare their required capabilities in metadata.json.
Updates checksum to match the new canonical metadata.
@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review this PR

Copy link
Copy Markdown
Owner

@utksh1 utksh1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the latest capability-policy update. This still needs tightening before merge. The PR changes capability metadata for the full plugin catalog in the same branch as the enforcement engine, which makes individual capability assignments hard to audit. Please split or at least add a generated/checked inventory test that verifies every plugin has only recognized capabilities and that high-risk plugins are classified consistently. Also validate operator-denied capability tokens at settings/enforcer construction time; a typo in SECUSCAN_DENIED_CAPABILITIES should fail loudly instead of silently producing a policy that does not enforce what the operator intended.

@advikdivekar
Copy link
Copy Markdown
Contributor Author

Thanks for the latest capability-policy update. This still needs tightening before merge. The PR changes capability metadata for the full plugin catalog in the same branch as the enforcement engine, which makes individual capability assignments hard to audit. Please split or at least add a generated/checked inventory test that verifies every plugin has only recognized capabilities and that high-risk plugins are classified consistently. Also validate operator-denied capability tokens at settings/enforcer construction time; a typo in SECUSCAN_DENIED_CAPABILITIES should fail loudly instead of silently producing a policy that does not enforce what the operator intended.

Okay, I'll start working on it

…inventory tests

Two changes:
1. CapabilityEnforcer now raises ValueError at construction time when
   SECUSCAN_DENIED_CAPABILITIES contains an unrecognised token. A typo in
   the deny list previously silently produced an empty deny set, which would
   fail to enforce the intended policy.

2. New inventory test suite scans every plugin metadata.json and asserts:
   - All declared capability tokens are in ALL_CAPABILITIES
   - Exploit-level plugins that declare explicit capabilities include "exploit"
   - Intrusive-level plugins that declare explicit capabilities include "intrusive"
   - The capabilities field is a JSON array of lowercase strings
   CapabilityEnforcer construction-time validation is also fully covered.
Copy link
Copy Markdown
Owner

@utksh1 utksh1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the latest head. Still blocked: the current diff still carries broad plugin metadata/checksum churn while only one plugin ends up with an explicit capabilities example, so the security policy is difficult to audit and easy to regress. Please narrow this to the enforcement code plus a deliberate metadata migration, or add an inventory/regression test proving every shipped plugin has the expected effective capabilities and that denied capabilities block execution before command construction. Also avoid unrelated metadata rewrites such as icon encoding/order/checksum churn unless they are part of a scripted, documented migration.

…locking tests

metadata fix:
  Revert waf_detector/metadata.json to upstream key order, icon encoding, and
  name. The only intentional change from upstream is adding `capabilities:
  ["network"]` and updating the checksum. This eliminates all unrelated
  encoding/ordering churn from the diff.

inventory tests (32 total):
  - effective_capabilities implied-set correctness for all safety levels
    (safe/intrusive/exploit/unknown) and the explicit-override behaviour
  - All shipped plugins produce a non-empty effective_capabilities set
  - CapabilityEnforcer.check() raises CapabilityDeniedError BEFORE any
    fake build_command is called (execution-blocking regression)
  - Exploit/intrusive plugins blocked when the matching cap is denied
  - Safe plugin passes when only exploit is denied
  - Plugin with explicit capabilities declaration blocked on denied token
  - Empty deny-list never blocks any shipped plugin
  - Denying "exploit" blocks EVERY exploit-level plugin in the plugins/ tree
@advikdivekar
Copy link
Copy Markdown
Contributor Author

@utksh1 please review this PR once

Copy link
Copy Markdown
Owner

@utksh1 utksh1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the latest head after the inventory update and main merge. The PR now includes capability inventory coverage, denied-token validation, execution-blocking regression coverage, and green backend/frontend checks. Good to merge.

@utksh1 utksh1 added the gssoc:approved Admin validation: approved for GSSoC scoring label Jun 1, 2026
@utksh1 utksh1 merged commit c7c6099 into utksh1:main Jun 1, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:backend Backend API, database, or service work area:plugins Scanner plugin metadata, schemas, or plugin runtime work area:security Security-sensitive implementation or tests gssoc:approved Admin validation: approved for GSSoC scoring level:critical 80 pts difficulty label for critical or high-impact PRs type:feature Feature work category bonus label type:security Security work category bonus label type:testing Testing work category bonus label

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY] Add per-plugin capability permissions and enforcement

2 participants