Security Scanning for Plugin Contributors¶
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¶
| Severity | Emoji | Action | Examples |
|---|---|---|---|
| Critical | 🔴 | BLOCKS MERGE | Hardcoded secrets, RCE vulnerabilities, CVSS ≥ 9.0 |
| High | 🟡 | BLOCKS MERGE | CVE CVSS 7.0-8.9, command injection, SQL injection |
| Medium | 🟠 | Warning | CVE CVSS 4.0-6.9, weak crypto, missing validation |
| Low | 🟢 | Info | CVE 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¶
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¶
| Field | Required | Description |
|---|---|---|
reason | ✅ Yes | Why this is safe (minimum 10 characters) |
approved_by | ⚠️ For Critical/High | Who approved this exemption |
ticket | ⚠️ For Critical/High | Tracking ticket (ADO work item) |
expires | ⚠️ If temporary: true | ISO 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¶
Fix Option 1 - Use allowed pattern:
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:
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¶
| Tool | Purpose | Language |
|---|---|---|
| detect-secrets | Secret detection | All |
| pip-audit | Python CVE scanning | Python |
| npm audit | Node.js CVE scanning | Node.js |
| bandit | Python SAST | Python |
Getting Help¶
If you're blocked by security findings:
- Review the error message - it includes specific recommendations
- Check if it's a false positive - add to
.security-exemptions.json - Ask in the PR - tag @security-team for guidance
- 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 ofeval() - Store test credentials in fixture files (excluded from scanning)
DON'T ❌¶
- Hardcode secrets in source code
- Use
eval()orexec()with user input - Use
os.system()orshell=Truein subprocess - Trust user input without validation
- Use
picklefor 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
PluginInstallerenforces 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
signaturefield → install raisesMarketplaceError - A plugin signed by an author not in
trusted_keys→ install raisesMarketplaceError - 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_keysis now frozen at construction usingtypes.MappingProxyType. Code that previously mutatedinstaller._trusted_keysafter construction will raiseTypeError. Pass the complete key mapping at construction time instead.
Development Override¶
To install an unsigned plugin during development, pass verify=False explicitly:
🔴 CAUTION: Never use
verify=Falsein production. It disables signature verification entirely, allowing any plugin — including potentially malicious ones — to be installed. Gate this behind an environment check:
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
securitylabel