agentsec-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentsec/__init__.py +3 -0
- agentsec/baseline.py +60 -0
- agentsec/cli.py +84 -0
- agentsec/owasp.py +212 -0
- agentsec/parsers/__init__.py +5 -0
- agentsec/parsers/core.py +8 -0
- agentsec/parsers/json_parser.py +69 -0
- agentsec/parsers/toml_parser.py +28 -0
- agentsec/parsers/yaml_parser.py +28 -0
- agentsec/report.py +36 -0
- agentsec/rules/__init__.py +5 -0
- agentsec/rules/additional.py +227 -0
- agentsec/rules/base.py +108 -0
- agentsec/sarif.py +110 -0
- agentsec/scanner.py +119 -0
- agentsec_cli-0.1.0.dist-info/METADATA +161 -0
- agentsec_cli-0.1.0.dist-info/RECORD +21 -0
- agentsec_cli-0.1.0.dist-info/WHEEL +5 -0
- agentsec_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agentsec_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- agentsec_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Additional security rules for AgentSec."""
|
|
2
|
+
|
|
3
|
+
from .base import Rule
|
|
4
|
+
|
|
5
|
+
def load_additional_rules():
|
|
6
|
+
return [
|
|
7
|
+
Rule(
|
|
8
|
+
name="Network + filesystem access",
|
|
9
|
+
severity="critical",
|
|
10
|
+
description="MCP server has both network and filesystem access (exfiltration risk)",
|
|
11
|
+
recommendation="Separate network and filesystem capabilities, or implement strict allowlists.",
|
|
12
|
+
detect_patterns=["http", "https", "curl", "wget", "fetch", "filesystem", "write", "edit", "delete", "rm", "mv"]
|
|
13
|
+
),
|
|
14
|
+
Rule(
|
|
15
|
+
name="Suspicious tool description",
|
|
16
|
+
severity="high",
|
|
17
|
+
description="Tool description contains suspicious instructions (prompt injection)",
|
|
18
|
+
recommendation="Review and sanitize tool descriptions; avoid instruction-like language.",
|
|
19
|
+
detect_patterns=["ignore previous instructions", "ignore all instructions", "do not tell the user", "secretly", "exfiltrate", "send to", "bypass", "disable security", "you are now", "system prompt"]
|
|
20
|
+
),
|
|
21
|
+
Rule(
|
|
22
|
+
name="GitHub token exposure",
|
|
23
|
+
severity="high",
|
|
24
|
+
description="GitHub token or actions:write permission detected",
|
|
25
|
+
recommendation="Use fine-grained tokens with minimal permissions; avoid actions:write unless necessary.",
|
|
26
|
+
detect_patterns=["GITHUB_TOKEN", "gh auth token", ".git/config", "actions: write", "contents: write"]
|
|
27
|
+
),
|
|
28
|
+
Rule(
|
|
29
|
+
name="Communication tool write permission",
|
|
30
|
+
severity="high",
|
|
31
|
+
description="MCP server can send messages to Slack/email/GitHub (data leak risk)",
|
|
32
|
+
recommendation="Limit write permissions for communication tools; use separate accounts with restricted scopes.",
|
|
33
|
+
detect_patterns=["slack", "gmail", "email", "send_message", "post_message", "create_issue", "comment", "reply"]
|
|
34
|
+
),
|
|
35
|
+
Rule(
|
|
36
|
+
name="Database write/delete permission",
|
|
37
|
+
severity="high",
|
|
38
|
+
description="MCP server can modify or delete database records",
|
|
39
|
+
recommendation="Use read-only credentials for MCP servers; require manual approval for destructive operations.",
|
|
40
|
+
detect_patterns=["postgres", "mysql", "mongodb", "redis", "delete", "drop", "update", "insert"]
|
|
41
|
+
),
|
|
42
|
+
Rule(
|
|
43
|
+
name="Unpinned dependency",
|
|
44
|
+
severity="medium",
|
|
45
|
+
description="MCP server dependency not pinned to a specific version",
|
|
46
|
+
recommendation="Pin dependencies to a specific version or commit SHA to avoid supply-chain attacks.",
|
|
47
|
+
detect_patterns=["latest", ":latest", "@latest"]
|
|
48
|
+
),
|
|
49
|
+
Rule(
|
|
50
|
+
name="Remote script install",
|
|
51
|
+
severity="high",
|
|
52
|
+
description="Remote script installation pattern (curl | bash) detected",
|
|
53
|
+
recommendation="Avoid piping remote scripts to shell; prefer verified packages or download and inspect.",
|
|
54
|
+
detect_patterns=["curl ... | bash", "wget ... | sh"]
|
|
55
|
+
),
|
|
56
|
+
Rule(
|
|
57
|
+
name="Excessive autonomy instruction",
|
|
58
|
+
severity="medium",
|
|
59
|
+
description="Agent instruction requests excessive autonomy (no confirmation)",
|
|
60
|
+
recommendation="Require user confirmation for important actions; avoid 'auto-approve' instructions.",
|
|
61
|
+
detect_patterns=["do not ask for confirmation", "always run commands", "auto-approve", "never ask user", "full access", "without confirmation"]
|
|
62
|
+
),
|
|
63
|
+
Rule(
|
|
64
|
+
name="Prompt injection in markdown",
|
|
65
|
+
severity="medium",
|
|
66
|
+
description="Markdown file contains potential prompt injection phrases",
|
|
67
|
+
recommendation="Sanitize agent instructions; avoid embedding system-level directives in markdown.",
|
|
68
|
+
detect_patterns=["ignore previous instructions", "as an AI agent", "system prompt", "developer message", "hidden instruction"]
|
|
69
|
+
),
|
|
70
|
+
Rule(
|
|
71
|
+
name="Sensitive file reference",
|
|
72
|
+
severity="high",
|
|
73
|
+
description="Agent instruction references sensitive files or secrets",
|
|
74
|
+
recommendation="Remove references to secrets or use gitignored files for credentials.",
|
|
75
|
+
detect_patterns=[".env", "id_rsa", ".ssh", "credentials", "secrets", "tokens", "auth.json"]
|
|
76
|
+
),
|
|
77
|
+
Rule(
|
|
78
|
+
name="MCP OAuth broad scopes",
|
|
79
|
+
severity="medium",
|
|
80
|
+
description="MCP OAuth configuration has overly broad scopes",
|
|
81
|
+
recommendation="Use minimal required scopes; avoid '*' or 'admin' scopes.",
|
|
82
|
+
detect_patterns=["oauth", "*", "admin", "full_access"]
|
|
83
|
+
),
|
|
84
|
+
Rule(
|
|
85
|
+
name="Web + filesystem access",
|
|
86
|
+
severity="high",
|
|
87
|
+
description="Tool can browse web and write files (prompt injection to file write risk)",
|
|
88
|
+
recommendation="Isolate web browsing from filesystem write; use separate tools with restricted permissions.",
|
|
89
|
+
detect_patterns=["http", "https", "curl", "wget", "fetch", "write", "edit", "delete", "rm", "mv"]
|
|
90
|
+
),
|
|
91
|
+
Rule(
|
|
92
|
+
name="Read repo + network",
|
|
93
|
+
severity="high",
|
|
94
|
+
description="Tool can read repo files and send network requests (exfiltration risk)",
|
|
95
|
+
recommendation="Restrict read access for network-capable tools; use separate credentials.",
|
|
96
|
+
detect_patterns=["read_file", "read", "http", "https", "curl", "wget", "fetch"]
|
|
97
|
+
),
|
|
98
|
+
Rule(
|
|
99
|
+
name="Unknown/untrusted source",
|
|
100
|
+
severity="medium",
|
|
101
|
+
description="MCP server package from unknown or untrusted source",
|
|
102
|
+
recommendation="Use packages from trusted registries (npm, PyPI) with known maintainers.",
|
|
103
|
+
detect_patterns=["raw.githubusercontent.com", r"raw\.", "gist", "pastebin", "dropbox", "bitbucket"]
|
|
104
|
+
),
|
|
105
|
+
Rule(
|
|
106
|
+
name="No policy file",
|
|
107
|
+
severity="low",
|
|
108
|
+
description="Project uses MCP/tools but no local security policy file",
|
|
109
|
+
recommendation="Define a policy file (e.g., .agentsec.yaml) to specify allow/deny lists.",
|
|
110
|
+
detect_patterns=["mcpServers", "tools", "allowed", "denied"]
|
|
111
|
+
),
|
|
112
|
+
# Новые правила для популярных AI-агентов и дополнительных уязвимостей
|
|
113
|
+
Rule(
|
|
114
|
+
name="Cursor agent config with dangerous permissions",
|
|
115
|
+
severity="high",
|
|
116
|
+
description="Cursor agent configuration grants dangerous permissions (shell, filesystem write, network)",
|
|
117
|
+
recommendation="Restrict permissions for Cursor agent to minimal required; use project-specific configs.",
|
|
118
|
+
detect_patterns=["cursor-agent"],
|
|
119
|
+
all_patterns=["cursor", "shell", "write", "network"]
|
|
120
|
+
),
|
|
121
|
+
Rule(
|
|
122
|
+
name="Claude Desktop config with MCP server risks",
|
|
123
|
+
severity="high",
|
|
124
|
+
description="Claude Desktop MCP server configuration has risky settings (shell, filesystem, network)",
|
|
125
|
+
recommendation="Review Claude Desktop MCP config; restrict shell and filesystem access.",
|
|
126
|
+
detect_patterns=["claude-desktop", "claude_desktop", "claude_desktop_config.json"],
|
|
127
|
+
all_patterns=["mcpservers", "filesystem"]
|
|
128
|
+
),
|
|
129
|
+
Rule(
|
|
130
|
+
name="Codex/Cline agent with unrestricted tools",
|
|
131
|
+
severity="high",
|
|
132
|
+
description="Codex or Cline agent has unrestricted access to tools (shell, filesystem, network)",
|
|
133
|
+
recommendation="Restrict tool access for Codex/Cline agents; use permission prompts.",
|
|
134
|
+
detect_patterns=["codex", "cline", "tools", "shell", "filesystem", "network", "permissions"]
|
|
135
|
+
),
|
|
136
|
+
Rule(
|
|
137
|
+
name="Environment variable exposure",
|
|
138
|
+
severity="critical",
|
|
139
|
+
description="MCP server or agent config exposes environment variables with secrets",
|
|
140
|
+
recommendation="Avoid exposing env vars in configs; use .env files and gitignore them.",
|
|
141
|
+
detect_patterns=["process.env", "AWS_", "OPENAI_", "ANTHROPIC_", "GOOGLE_", "GITHUB_", "SLACK_", "DISCORD_"]
|
|
142
|
+
),
|
|
143
|
+
Rule(
|
|
144
|
+
name="Vulnerable dependency pattern",
|
|
145
|
+
severity="medium",
|
|
146
|
+
description="MCP server dependency uses known vulnerable version pattern (e.g., outdated package)",
|
|
147
|
+
recommendation="Update dependencies to latest secure versions; use tools like npm audit or pip-audit.",
|
|
148
|
+
detect_patterns=["@modelcontextprotocol", "^0.1", "~0.0", "<1.0", ">=0.1"]
|
|
149
|
+
),
|
|
150
|
+
Rule(
|
|
151
|
+
name="Insecure default command",
|
|
152
|
+
severity="critical",
|
|
153
|
+
description="MCP server command uses insecure defaults (e.g., exec, eval, dangerous flags)",
|
|
154
|
+
recommendation="Avoid using eval, exec, or dangerous command-line flags; use safe alternatives.",
|
|
155
|
+
detect_patterns=["eval", "exec", "-e", "-c", "--eval", "--exec", "child_process"]
|
|
156
|
+
),
|
|
157
|
+
Rule(
|
|
158
|
+
name="Read-only file system in MCP server",
|
|
159
|
+
severity="medium",
|
|
160
|
+
description="MCP server has read-only filesystem access but may still expose sensitive files",
|
|
161
|
+
recommendation="Even read-only access can leak secrets; restrict path to necessary directories.",
|
|
162
|
+
detect_patterns=["read", "readonly", "read-only", "filesystem", "path"]
|
|
163
|
+
),
|
|
164
|
+
Rule(
|
|
165
|
+
name="Missing input validation",
|
|
166
|
+
severity="medium",
|
|
167
|
+
description="Agent or MCP server lacks input validation, potentially allowing injection attacks",
|
|
168
|
+
recommendation="Validate and sanitize all inputs from the agent or external sources.",
|
|
169
|
+
detect_patterns=["input", "prompt", "argument", "parameter", "validate", "sanitize"]
|
|
170
|
+
),
|
|
171
|
+
Rule(
|
|
172
|
+
name="Package manager execution",
|
|
173
|
+
severity="high",
|
|
174
|
+
description="Agent config invokes package managers that can execute install scripts",
|
|
175
|
+
recommendation="Pin packages, disable lifecycle scripts where possible, and avoid dynamic package execution.",
|
|
176
|
+
detect_patterns=["npx", "uvx", "pipx", "bunx", "pnpm dlx", "npm exec"]
|
|
177
|
+
),
|
|
178
|
+
Rule(
|
|
179
|
+
name="Container privileged mode",
|
|
180
|
+
severity="critical",
|
|
181
|
+
description="Containerized MCP server may run with elevated host privileges",
|
|
182
|
+
recommendation="Avoid privileged mode and host namespace sharing; use minimal container capabilities.",
|
|
183
|
+
detect_patterns=["privileged: true", "--privileged", "pid: host", "network_mode: host", "--network=host"]
|
|
184
|
+
),
|
|
185
|
+
Rule(
|
|
186
|
+
name="Host mount exposure",
|
|
187
|
+
severity="critical",
|
|
188
|
+
description="MCP server or container mounts sensitive host directories",
|
|
189
|
+
recommendation="Mount only required project directories and prefer read-only mounts.",
|
|
190
|
+
detect_patterns=["/var/run/docker.sock", "/:/", "/home:/", "/root:/", "~:/", "/Users:/"]
|
|
191
|
+
),
|
|
192
|
+
Rule(
|
|
193
|
+
name="Browser automation with local file access",
|
|
194
|
+
severity="high",
|
|
195
|
+
description="Agent config combines browser automation with local file access",
|
|
196
|
+
recommendation="Isolate browser automation from sensitive filesystem paths.",
|
|
197
|
+
detect_patterns=["playwright", "puppeteer", "browser", "filesystem", "file://"]
|
|
198
|
+
),
|
|
199
|
+
Rule(
|
|
200
|
+
name="Dynamic code execution",
|
|
201
|
+
severity="critical",
|
|
202
|
+
description="Agent or MCP server can dynamically evaluate code",
|
|
203
|
+
recommendation="Avoid eval-style execution and route code execution through reviewed, sandboxed tools.",
|
|
204
|
+
detect_patterns=["eval(", "exec(", "new Function", "child_process.exec", "python -c", "node -e"]
|
|
205
|
+
),
|
|
206
|
+
Rule(
|
|
207
|
+
name="Wildcard tool allowlist",
|
|
208
|
+
severity="high",
|
|
209
|
+
description="Agent config appears to allow all tools or permissions via wildcard",
|
|
210
|
+
recommendation="Use explicit allowlists for tools, paths, hosts, and permissions.",
|
|
211
|
+
detect_patterns=["allowed_tools: *", "allow_all", "allowed: [\"*\"]", "permissions: *", "tools: *"]
|
|
212
|
+
),
|
|
213
|
+
Rule(
|
|
214
|
+
name="Telemetry or analytics endpoint",
|
|
215
|
+
severity="medium",
|
|
216
|
+
description="Agent or MCP server references telemetry or analytics endpoints",
|
|
217
|
+
recommendation="Ensure telemetry is opt-in and never includes prompts, secrets, or source code.",
|
|
218
|
+
detect_patterns=["telemetry", "analytics", "posthog", "segment", "sentry", "datadog"]
|
|
219
|
+
),
|
|
220
|
+
Rule(
|
|
221
|
+
name="Credential helper access",
|
|
222
|
+
severity="high",
|
|
223
|
+
description="Agent config references credential stores or auth helpers",
|
|
224
|
+
recommendation="Do not expose credential helpers to agents; use scoped tokens with least privilege.",
|
|
225
|
+
detect_patterns=["credential.helper", "keychain", "secretservice", "wincred", "gh auth", "aws configure"]
|
|
226
|
+
),
|
|
227
|
+
]
|
agentsec/rules/base.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Base rule definitions."""
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
class Rule:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
name: str,
|
|
10
|
+
severity: str,
|
|
11
|
+
description: str,
|
|
12
|
+
recommendation: str,
|
|
13
|
+
detect_patterns: List[str],
|
|
14
|
+
all_patterns: List[str] | None = None,
|
|
15
|
+
):
|
|
16
|
+
self.name = name
|
|
17
|
+
self.severity = severity
|
|
18
|
+
self.description = description
|
|
19
|
+
self.recommendation = recommendation
|
|
20
|
+
self.patterns = detect_patterns
|
|
21
|
+
self.all_patterns = all_patterns or []
|
|
22
|
+
|
|
23
|
+
def detect(self, content: str, file_path: Path) -> bool:
|
|
24
|
+
if not content:
|
|
25
|
+
return False
|
|
26
|
+
lower = content.lower()
|
|
27
|
+
if self.all_patterns:
|
|
28
|
+
return all(pattern.lower() in lower for pattern in self.all_patterns)
|
|
29
|
+
for pattern in self.patterns:
|
|
30
|
+
if pattern.lower() in lower:
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
def load_rules() -> List[Rule]:
|
|
35
|
+
from .additional import load_additional_rules
|
|
36
|
+
base_rules = [
|
|
37
|
+
Rule(
|
|
38
|
+
name="MCP shell execution",
|
|
39
|
+
severity="critical",
|
|
40
|
+
description="MCP server can execute shell commands",
|
|
41
|
+
recommendation="Require explicit approval or remove shell access.",
|
|
42
|
+
detect_patterns=["bash", "sh", "powershell", "cmd", "exec", "subprocess", "terminal", "run_command"]
|
|
43
|
+
),
|
|
44
|
+
Rule(
|
|
45
|
+
name="MCP filesystem write access",
|
|
46
|
+
severity="critical",
|
|
47
|
+
description="MCP server has filesystem write access",
|
|
48
|
+
recommendation="Restrict filesystem access to read-only or specific directories.",
|
|
49
|
+
detect_patterns=["filesystem", "write", "edit", "delete", "rm", "mv", "path", "workspace"]
|
|
50
|
+
),
|
|
51
|
+
Rule(
|
|
52
|
+
name="Secret exposure",
|
|
53
|
+
severity="critical",
|
|
54
|
+
description="MCP server can access secrets or environment variables",
|
|
55
|
+
recommendation="Do not expose secrets to MCP servers; use environment variables with caution.",
|
|
56
|
+
detect_patterns=[".env", "process.env", "AWS_SECRET_ACCESS_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GITHUB_TOKEN", "SLACK_BOT_TOKEN"]
|
|
57
|
+
),
|
|
58
|
+
Rule(
|
|
59
|
+
name="Broad path access",
|
|
60
|
+
severity="high",
|
|
61
|
+
description="MCP server has broad filesystem path access (root or home)",
|
|
62
|
+
recommendation="Restrict path to specific directories.",
|
|
63
|
+
detect_patterns=["/", "~", "/home", "/Users", "C:\\", ".", "..", "**"]
|
|
64
|
+
),
|
|
65
|
+
Rule(
|
|
66
|
+
name="Prompt injection risk",
|
|
67
|
+
severity="medium",
|
|
68
|
+
description="Agent instruction file contains suspicious prompt injection patterns",
|
|
69
|
+
recommendation="Review and sanitize agent instruction files.",
|
|
70
|
+
detect_patterns=["ignore previous instructions", "ignore all instructions", "do not tell the user", "secretly", "exfiltrate", "bypass", "disable security", "you are now", "system prompt", "hidden instruction"]
|
|
71
|
+
),
|
|
72
|
+
Rule(
|
|
73
|
+
name="Sensitive file reference",
|
|
74
|
+
severity="high",
|
|
75
|
+
description="Agent instruction references sensitive files or secrets",
|
|
76
|
+
recommendation="Remove references to secrets or use gitignored files.",
|
|
77
|
+
detect_patterns=[".env", "id_rsa", ".ssh", "credentials", "secrets", "tokens", "auth.json"]
|
|
78
|
+
),
|
|
79
|
+
Rule(
|
|
80
|
+
name="Excessive autonomy",
|
|
81
|
+
severity="medium",
|
|
82
|
+
description="Agent instruction asks for excessive autonomy (no confirmation)",
|
|
83
|
+
recommendation="Require user confirmation for important actions.",
|
|
84
|
+
detect_patterns=["do not ask for confirmation", "always run commands", "auto-approve", "never ask user", "full access"]
|
|
85
|
+
),
|
|
86
|
+
Rule(
|
|
87
|
+
name="Unpinned dependency",
|
|
88
|
+
severity="medium",
|
|
89
|
+
description="MCP server dependency is not pinned (latest tag or no version)",
|
|
90
|
+
recommendation="Pin dependencies to a specific version or commit SHA.",
|
|
91
|
+
detect_patterns=["latest", ":latest", "@latest"]
|
|
92
|
+
),
|
|
93
|
+
Rule(
|
|
94
|
+
name="Remote script install",
|
|
95
|
+
severity="high",
|
|
96
|
+
description="Agent config uses remote script install pattern (curl | bash)",
|
|
97
|
+
recommendation="Avoid piping remote scripts directly to shell.",
|
|
98
|
+
detect_patterns=["curl ... | bash", "wget ... | sh"]
|
|
99
|
+
),
|
|
100
|
+
Rule(
|
|
101
|
+
name="Docker socket access",
|
|
102
|
+
severity="critical",
|
|
103
|
+
description="MCP server can access the Docker socket",
|
|
104
|
+
recommendation="Avoid mounting the Docker socket unless absolutely necessary.",
|
|
105
|
+
detect_patterns=["/var/run/docker.sock", "docker.sock"]
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
return base_rules + load_additional_rules()
|
agentsec/sarif.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""SARIF (Static Analysis Results Interchange Format) report generator.
|
|
2
|
+
|
|
3
|
+
Conforms to SARIF v2.1.0 (https://docs.oasis-open.org/sarif/sarif/v2.1.0/).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import datetime
|
|
8
|
+
from typing import List, Dict, Any
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_sarif(findings: List[Dict[str, Any]], repo_root: str = ".") -> Dict[str, Any]:
|
|
13
|
+
"""Convert AgentSec findings to SARIF format."""
|
|
14
|
+
|
|
15
|
+
rules = {}
|
|
16
|
+
results = []
|
|
17
|
+
|
|
18
|
+
severity_mapping = {
|
|
19
|
+
"critical": 3.0,
|
|
20
|
+
"high": 2.0,
|
|
21
|
+
"medium": 1.0,
|
|
22
|
+
"low": 0.5,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for idx, finding in enumerate(findings):
|
|
26
|
+
rule_id = finding['rule'].replace(" ", "_").lower()
|
|
27
|
+
|
|
28
|
+
if rule_id not in rules:
|
|
29
|
+
desc = finding['description']
|
|
30
|
+
owasp = finding.get('owasp', '')
|
|
31
|
+
full_desc = desc
|
|
32
|
+
if owasp:
|
|
33
|
+
full_desc = f"[{owasp}] {desc}"
|
|
34
|
+
|
|
35
|
+
rules[rule_id] = {
|
|
36
|
+
"id": rule_id,
|
|
37
|
+
"shortDescription": {"text": finding['rule']},
|
|
38
|
+
"fullDescription": {"text": full_desc},
|
|
39
|
+
"defaultConfiguration": {"level": "warning"},
|
|
40
|
+
"help": {
|
|
41
|
+
"text": finding['recommendation'],
|
|
42
|
+
"markdown": f"**Recommendation:** {finding['recommendation']}"
|
|
43
|
+
},
|
|
44
|
+
"properties": {
|
|
45
|
+
"precision": "very-high",
|
|
46
|
+
"security-severity": str(severity_mapping.get(finding['severity'].lower(), 1.0)),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if owasp:
|
|
50
|
+
rules[rule_id]["properties"]["owasp"] = owasp
|
|
51
|
+
|
|
52
|
+
result = {
|
|
53
|
+
"ruleId": rule_id,
|
|
54
|
+
"level": "warning" if finding['severity'].lower() in ["medium", "low"] else "error",
|
|
55
|
+
"message": {
|
|
56
|
+
"text": f"{finding['description']} — {finding['recommendation']}"
|
|
57
|
+
},
|
|
58
|
+
"locations": [
|
|
59
|
+
{
|
|
60
|
+
"physicalLocation": {
|
|
61
|
+
"artifactLocation": {
|
|
62
|
+
"uri": finding['file']
|
|
63
|
+
},
|
|
64
|
+
"region": {
|
|
65
|
+
"startLine": 1,
|
|
66
|
+
"endLine": 1
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"properties": {
|
|
72
|
+
"severity": finding['severity'].upper(),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if finding.get('owasp'):
|
|
76
|
+
result["properties"]["owasp"] = finding['owasp']
|
|
77
|
+
if finding.get('server'):
|
|
78
|
+
result["properties"]["server"] = finding['server']
|
|
79
|
+
|
|
80
|
+
results.append(result)
|
|
81
|
+
|
|
82
|
+
sarif_doc = {
|
|
83
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
84
|
+
"version": "2.1.0",
|
|
85
|
+
"runs": [
|
|
86
|
+
{
|
|
87
|
+
"tool": {
|
|
88
|
+
"driver": {
|
|
89
|
+
"name": "AgentSec",
|
|
90
|
+
"organization": "AgentSec",
|
|
91
|
+
"informationUri": "https://github.com/locface/AgentSec",
|
|
92
|
+
"rules": list(rules.values()),
|
|
93
|
+
"version": "0.2.0"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"results": results,
|
|
97
|
+
"properties": {
|
|
98
|
+
"startTimeUtc": datetime.datetime.now(datetime.UTC).isoformat().replace("+00:00", "Z")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return sarif_doc
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def print_sarif(findings: List[Dict[str, Any]], repo_root: str = ".") -> None:
|
|
108
|
+
"""Print SARIF report to stdout."""
|
|
109
|
+
sarif = generate_sarif(findings, repo_root)
|
|
110
|
+
print(json.dumps(sarif, indent=2))
|
agentsec/scanner.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Main scanner orchestrator."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
from .rules import Rule, load_rules
|
|
5
|
+
from .parsers import parse_file
|
|
6
|
+
from .parsers.json_parser import parse_mcp_config as parse_json_mcp
|
|
7
|
+
from .parsers.yaml_parser import parse_mcp_config as parse_yaml_mcp
|
|
8
|
+
from .parsers.toml_parser import parse_mcp_config as parse_toml_mcp
|
|
9
|
+
from .owasp import get_owasp_ids
|
|
10
|
+
|
|
11
|
+
class Scanner:
|
|
12
|
+
def __init__(self, root: Path, include_hidden: bool = False, min_severity: str = "all"):
|
|
13
|
+
self.root = root
|
|
14
|
+
self.include_hidden = include_hidden
|
|
15
|
+
self.min_severity = min_severity
|
|
16
|
+
self.rules = load_rules()
|
|
17
|
+
|
|
18
|
+
def scan(self) -> List[Dict[str, Any]]:
|
|
19
|
+
"""Walk the directory, parse relevant files, and apply rules."""
|
|
20
|
+
findings = []
|
|
21
|
+
target_patterns = [
|
|
22
|
+
"mcp.json", "mcp.yaml", "mcp.yml", "mcp.toml",
|
|
23
|
+
"mcp-config.json", "claude_desktop_config.json", "settings.json",
|
|
24
|
+
"AGENTS.md", "CLAUDE.md", ".cursorrules", ".cursor/rules",
|
|
25
|
+
".cursor", "cline_mcp", ".clinerules", "codex.toml",
|
|
26
|
+
".env", ".env.example",
|
|
27
|
+
"docker-compose.yml", "Dockerfile",
|
|
28
|
+
"package.json", "requirements.txt"
|
|
29
|
+
]
|
|
30
|
+
mcp_patterns = [
|
|
31
|
+
"mcp.json", "mcp.yaml", "mcp.yml", "mcp.toml",
|
|
32
|
+
"mcp-config.json", "claude_desktop_config.json", "settings.json", "cline_mcp"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
for file_path in self.root.rglob("*"):
|
|
36
|
+
if not file_path.is_file():
|
|
37
|
+
continue
|
|
38
|
+
if not self.include_hidden and file_path.name.startswith(".") and file_path.name not in [".env", ".env.example"]:
|
|
39
|
+
continue
|
|
40
|
+
|
|
41
|
+
if not any(p in str(file_path) for p in target_patterns):
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
content = parse_file(file_path)
|
|
45
|
+
if content is None:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if any(mcp_pattern in str(file_path) for mcp_pattern in mcp_patterns):
|
|
49
|
+
mcp_data = None
|
|
50
|
+
if file_path.suffix == ".json":
|
|
51
|
+
mcp_data = parse_json_mcp(content, file_path)
|
|
52
|
+
elif file_path.suffix in [".yaml", ".yml"]:
|
|
53
|
+
mcp_data = parse_yaml_mcp(content, file_path)
|
|
54
|
+
elif file_path.suffix == ".toml":
|
|
55
|
+
mcp_data = parse_toml_mcp(content, file_path)
|
|
56
|
+
if mcp_data:
|
|
57
|
+
for server in mcp_data:
|
|
58
|
+
for rule in self.rules:
|
|
59
|
+
if self._apply_rule_to_mcp_server(rule, server):
|
|
60
|
+
findings.append(self._make_finding(file_path, rule, server))
|
|
61
|
+
else:
|
|
62
|
+
for rule in self.rules:
|
|
63
|
+
if rule.detect(content, file_path):
|
|
64
|
+
findings.append(self._make_finding(file_path, rule))
|
|
65
|
+
|
|
66
|
+
return findings
|
|
67
|
+
|
|
68
|
+
def _make_finding(self, file_path: Path, rule: Rule, server: Dict[str, Any] | None = None) -> Dict[str, Any]:
|
|
69
|
+
"""Build a finding dict with OWASP info."""
|
|
70
|
+
finding = {
|
|
71
|
+
"file": str(file_path),
|
|
72
|
+
"rule": rule.name,
|
|
73
|
+
"severity": rule.severity,
|
|
74
|
+
"description": rule.description,
|
|
75
|
+
"recommendation": rule.recommendation,
|
|
76
|
+
"owasp": get_owasp_ids(rule.name),
|
|
77
|
+
}
|
|
78
|
+
if server:
|
|
79
|
+
finding["server"] = server.get("name", "unknown")
|
|
80
|
+
return finding
|
|
81
|
+
|
|
82
|
+
def _apply_rule_to_mcp_server(self, rule: Rule, server: Dict[str, Any]) -> bool:
|
|
83
|
+
"""Apply a rule to a structured MCP server entry."""
|
|
84
|
+
if rule.name == "MCP shell execution":
|
|
85
|
+
command = server.get("command", "").lower()
|
|
86
|
+
args = " ".join(server.get("args", [])).lower()
|
|
87
|
+
shell_indicators = ["bash", "sh", "powershell", "cmd", "zsh", "fish"]
|
|
88
|
+
if any(ind in command or ind in args for ind in shell_indicators):
|
|
89
|
+
return True
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
if rule.name == "MCP filesystem write access":
|
|
93
|
+
args = " ".join(server.get("args", [])).lower()
|
|
94
|
+
if server.get("command", "") == "npx" and "@modelcontextprotocol/server-filesystem" in args:
|
|
95
|
+
return True
|
|
96
|
+
write_indicators = ["write", "edit", "delete", "rm", "mv"]
|
|
97
|
+
if any(ind in args for ind in write_indicators):
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
if rule.name == "Secret exposure":
|
|
102
|
+
args = " ".join(server.get("args", [])).lower()
|
|
103
|
+
secret_patterns = [".env", "process.env", "aws_secret", "openai_api", "anthropic_api", "github_token", "slack_token"]
|
|
104
|
+
if any(p in args for p in secret_patterns):
|
|
105
|
+
return True
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
if rule.name == "Broad path access":
|
|
109
|
+
args = " ".join(server.get("args", [])).lower()
|
|
110
|
+
if "/" in args or ".." in args or "~" in args:
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
if rule.name == "Claude Desktop config with MCP server risks":
|
|
115
|
+
server_text = str(server).lower()
|
|
116
|
+
risky_indicators = ["filesystem", "shell", "bash", "network", "http", "https", "curl", "wget"]
|
|
117
|
+
return any(indicator in server_text for indicator in risky_indicators)
|
|
118
|
+
|
|
119
|
+
return rule.detect(str(server), Path("mcp"))
|