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
| 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.txtandpyproject.toml - Checks against OSV vulnerability database
- Reports CVEs with CVSS scores
Node.js (npm audit):
- Scans
package.jsonandpackage-lock.json - Checks npm advisory database
- Reports vulnerabilities with fix versions
3. Dangerous Code Patterns
Python (bandit):
eval(),exec()usagepickle.loads()deserializationos.system()instead of subprocess- Weak cryptography
- SQL injection patterns
- Insecure temporary files
JavaScript/TypeScript:
eval()usagenew Function()constructorinnerHTMLassignments (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
| 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+linenumber - Category match:
category+file(any line) - CVE match:
cveidentifier - Rule match: Tool-specific
ruleID (e.g.,B201for 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
| 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:
installer.install("my-dev-plugin", verify=False)
๐ด 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: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
securitylabel