Security Scanning for Plugin Contributors

August 4, 2026 ยท View on GitHub

This document explains the security scanning process that runs automatically on all plugin pull requests.

Overview

When you submit a PR that adds or modifies a plugin under plugins/, an automated security scan runs to detect:

  • ๐Ÿ”ด Hardcoded secrets (API keys, tokens, passwords)
  • ๐Ÿ”ด Dependency vulnerabilities (CVEs in Python/Node packages)
  • ๐ŸŸก Dangerous code patterns (eval, command injection, unsafe operations)
  • ๐ŸŸ  Unsafe file operations (path traversal, unrestricted writes)

Exit Behavior

The security scan uses the same exit behavior as other validation checks:

  • โŒ Critical/High severity findings block PR merge (exit code 1)
  • โš ๏ธ Medium/Low severity findings generate warnings but allow merge (exit code 0)
  • โœ… No findings allows PR to merge normally

Severity Levels

SeverityEmojiActionExamples
Critical๐Ÿ”ดBLOCKS MERGEHardcoded secrets, RCE vulnerabilities, CVSS โ‰ฅ 9.0
High๐ŸŸกBLOCKS MERGECVE CVSS 7.0-8.9, command injection, SQL injection
Medium๐ŸŸ WarningCVE CVSS 4.0-6.9, weak crypto, missing validation
Low๐ŸŸขInfoCVE CVSS < 4.0, best practice suggestions

What Gets Scanned

File Types

  • โœ… Python files (*.py)
  • โœ… JavaScript/TypeScript files (*.js, *.ts)
  • โœ… Shell scripts (*.sh, *.bash)
  • โœ… PowerShell scripts (*.ps1)
  • โœ… Dependency files (requirements.txt, package.json, pyproject.toml)
  • โœ… Code blocks in markdown files (skills and agents)

Exclusions

The scanner automatically skips:

  • โŒ Test fixtures and mock data (tests/fixtures/, **/*.test.py)
  • โŒ Example files (**/*.example.*, examples/, samples/)
  • โŒ Template files (**/*.template.*, **/*.sample.*)
  • โŒ Build artifacts (dist/, build/, node_modules/)
  • โŒ Documentation (most README.md, docs/**/*.md)

Note: Skills (skills/*/SKILL.md) and agents (agents/*.md) ARE scanned, including code blocks within them.

Security Checks

1. Secret Detection

Tool: detect-secrets

Catches:

  • API keys (AWS, Azure, Stripe, etc.)
  • Authentication tokens
  • Private keys
  • Database passwords
  • Connection strings

Allowed patterns (won't flag):

  • Test credentials: sk_test_*, test_key, mock_token
  • Localhost references: 127.0.0.1, localhost
  • Example domains: example.com

2. Dependency Vulnerability Scanning

Python (pip-audit):

  • Scans requirements.txt and pyproject.toml
  • Checks against OSV vulnerability database
  • Reports CVEs with CVSS scores

Node.js (npm audit):

  • Scans package.json and package-lock.json
  • Checks npm advisory database
  • Reports vulnerabilities with fix versions

3. Dangerous Code Patterns

Python (bandit):

  • eval(), exec() usage
  • pickle.loads() deserialization
  • os.system() instead of subprocess
  • Weak cryptography
  • SQL injection patterns
  • Insecure temporary files

JavaScript/TypeScript:

  • eval() usage
  • new Function() constructor
  • innerHTML assignments (XSS risk)

Shell scripts:

  • rm -rf / patterns
  • Pipe to bash from curl

Suppressing False Positives

If the scanner flags something that's actually safe, you can create a .security-exemptions.json file in your plugin directory.

Location

plugins/my-plugin/.security-exemptions.json

Format

{
  "version": "1.0",
  "exemptions": [
    {
      "tool": "detect-secrets",
      "file": "tests/fixtures/mock_credentials.py",
      "line": 12,
      "reason": "Test fixture with intentionally fake credentials for unit tests",
      "approved_by": "security-team"
    },
    {
      "tool": "bandit",
      "file": "server/dev_mode.py",
      "rule": "B201",
      "reason": "Flask debug mode only enabled in dev environment via FLASK_ENV check",
      "approved_by": "my-alias",
      "ticket": "ADO-12345"
    },
    {
      "tool": "pip-audit",
      "package": "requests",
      "version": "2.25.0",
      "cve": "CVE-2023-32681",
      "reason": "Not exploitable - only internal API calls, no proxy usage",
      "temporary": true,
      "expires": "2026-06-30",
      "ticket": "ADO-67890"
    }
  ]
}

Required Fields

FieldRequiredDescription
reasonโœ… YesWhy this is safe (minimum 10 characters)
approved_byโš ๏ธ For Critical/HighWho approved this exemption
ticketโš ๏ธ For Critical/HighTracking ticket (ADO work item)
expiresโš ๏ธ If temporary: trueISO date when exemption expires

Exemption Matching

Exemptions match findings by:

  • Exact match: file + line number
  • Category match: category + file (any line)
  • CVE match: cve identifier
  • Rule match: Tool-specific rule ID (e.g., B201 for bandit)

Temporary Exemptions

For known issues you plan to fix later:

{
  "tool": "pip-audit",
  "package": "old-library",
  "cve": "CVE-2025-12345",
  "reason": "Upgrade blocked by dependency conflict - scheduled for Q2",
  "temporary": true,
  "expires": "2026-06-30",
  "ticket": "ADO-99999"
}

Expired exemptions are treated as active findings again.

Common Scenarios

Scenario 1: Test Credentials in Fixtures

# tests/fixtures/mock_auth.py
TEST_API_KEY = "sk_test_fake_key_12345"  # โ† Will be flagged

Fix Option 1 - Use allowed pattern:

TEST_API_KEY = "sk_test_12345"  # Starts with sk_test_ (allowed)

Fix Option 2 - Add exemption:

{
  "tool": "detect-secrets",
  "file": "tests/fixtures/mock_auth.py",
  "line": 3,
  "reason": "Test fixture - not a real credential"
}

Scenario 2: Vulnerable Dependency

โŒ SECURITY SCAN FAILED

๐Ÿ”ด CRITICAL:
  [cve] Vulnerable dependency: requests==2.25.0
  โ”œโ”€ File: requirements.txt
  โ”œโ”€ CVE:  CVE-2023-32681 (CVSS 7.5)
  โ””โ”€ Fix:  Update to requests>=2.31.0

Fix:

# Update requirements.txt
requests>=2.31.0

# Or use pip-audit to fix automatically
pip-audit --fix

Scenario 3: eval() in Code Example

If you have eval() in a markdown code block that's demonstrative (showing what NOT to do):

Option 1 - Add a comment in the code block:

```python
# BAD EXAMPLE - DO NOT USE IN PRODUCTION
result = eval(user_input)  # Dangerous!

# GOOD EXAMPLE - Use instead:
result = ast.literal_eval(user_input)
```

Option 2 - Add exemption:

{
  "tool": "bandit",
  "file": "skills/security-examples/SKILL.md",
  "rule": "B307",
  "reason": "Demonstrative bad example showing what not to do"
}

Scenario 4: Legitimate Dynamic Code

Sometimes you genuinely need dynamic behavior:

# BEFORE (flagged)
exec(f"import {module_name}")

# BETTER (safer)
import importlib
module = importlib.import_module(module_name)

If you truly need exec() after reviewing alternatives:

{
  "tool": "bandit",
  "file": "server/plugin_loader.py",
  "line": 145,
  "rule": "B307",
  "reason": "Dynamic module loading from validated plugin registry - input sanitized with allowlist",
  "approved_by": "security-guardian",
  "ticket": "ADO-54321"
}

Running Locally

Before submitting your PR, run the security scan locally:

# From repository root
python scripts/sync-marketplace.py --preview

# Skip security scanning (not recommended)
python scripts/sync-marketplace.py --no-security --preview

# Test only security scanning
python scripts/security_scanner.py plugins/my-plugin --verbose

Tools Used

ToolPurposeLanguage
detect-secretsSecret detectionAll
pip-auditPython CVE scanningPython
npm auditNode.js CVE scanningNode.js
banditPython SASTPython

Getting Help

If you're blocked by security findings:

  1. Review the error message - it includes specific recommendations
  2. Check if it's a false positive - add to .security-exemptions.json
  3. Ask in the PR - tag @security-team for guidance
  4. Read the CWE/CVE - links provided in findings for detailed explanations

Best Practices

DO โœ…

  • Use environment variables for secrets: os.environ.get("API_KEY")
  • Keep dependencies updated
  • Use subprocess.run() with argument lists, not shell strings
  • Validate and sanitize all user input
  • Use ast.literal_eval() instead of eval()
  • Store test credentials in fixture files (excluded from scanning)

DON'T โŒ

  • Hardcode secrets in source code
  • Use eval() or exec() with user input
  • Use os.system() or shell=True in subprocess
  • Trust user input without validation
  • Use pickle for untrusted data
  • Leave vulnerable dependencies unfixed

Exemption Review

Security exemptions are reviewed:

  • Quarterly - Expired temporary exemptions trigger new findings
  • On promotion - All exemptions reviewed before moving to curated marketplace
  • Security audits - Periodic review of all exempted findings

Plugin Signature Enforcement (Fail-Closed)

Since PR #921, the PluginInstaller enforces a fail-closed policy for unsigned plugins.

When signature verification is enabled (the default), plugins without a valid Ed25519 signature are rejected at install time. This means:

  • A plugin with no signature field โ†’ install raises MarketplaceError
  • A plugin signed by an author not in trusted_keys โ†’ install raises MarketplaceError
  • A plugin whose signature does not verify โ†’ install raises MarketplaceError

Constructing a Trusted Key Store

from cryptography.hazmat.primitives.asymmetric import ed25519
from agentmesh.marketplace import PluginInstaller, PluginRegistry

# Load or generate the author's public key
public_key = ed25519.Ed25519PublicKey.from_public_bytes(raw_bytes)

# Construct the installer with a trusted key mapping.
# The mapping is frozen at construction time โ€” later mutations are rejected.
installer = PluginInstaller(
    plugins_dir=plugins_dir,
    registry=PluginRegistry(),
    trusted_keys={"author@example.com": public_key},
)

โš ๏ธ Breaking Change (v3.3+): trusted_keys is now frozen at construction using types.MappingProxyType. Code that previously mutated installer._trusted_keys after construction will raise TypeError. Pass the complete key mapping at construction time instead.

Development Override

To install an unsigned plugin during development, pass verify=False explicitly:

installer.install("my-dev-plugin", verify=False)

๐Ÿ”ด CAUTION: Never use verify=False in production. It disables signature verification entirely, allowing any plugin โ€” including potentially malicious ones โ€” to be installed. Gate this behind an environment check:

installer.install(name, verify=not os.getenv("AGT_PRODUCTION", False))

Python Version Requirements

The agent-sre SLI persistence module uses pathlib.Path.is_relative_to() for safe directory confinement checks. This method was introduced in Python 3.9. The package's pyproject.toml requires Python โ‰ฅ 3.10.

โš ๏ธ Breaking Change: If you are running Python < 3.9, path validation will raise AttributeError. Upgrade to Python 3.10+ (the minimum supported version).

If you encounter AttributeError: 'PosixPath' object has no attribute 'is_relative_to', upgrade your Python installation to 3.10 or later.

Questions?

  • Security concerns: Contact @security-team
  • False positives: Add exemption and document in PR
  • Tool issues: File an issue with the security label