agent-audit 0.1.0__py3-none-any.whl → 0.2.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.
- agent_audit/cli/commands/scan.py +41 -30
- agent_audit/cli/formatters/json.py +2 -2
- agent_audit/cli/formatters/sarif.py +9 -3
- agent_audit/models/finding.py +19 -7
- agent_audit/models/risk.py +13 -0
- agent_audit/rules/builtin/owasp_agentic.yaml +126 -0
- agent_audit/rules/builtin/owasp_agentic_v2.yaml +832 -0
- agent_audit/rules/engine.py +60 -1
- agent_audit/scanners/base.py +5 -3
- agent_audit/scanners/config_scanner.py +1 -1
- agent_audit/scanners/mcp_config_scanner.py +4 -3
- agent_audit/scanners/mcp_inspector.py +5 -4
- agent_audit/scanners/python_scanner.py +668 -7
- agent_audit/utils/mcp_client.py +1 -0
- agent_audit/version.py +1 -1
- {agent_audit-0.1.0.dist-info → agent_audit-0.2.0.dist-info}/METADATA +49 -35
- {agent_audit-0.1.0.dist-info → agent_audit-0.2.0.dist-info}/RECORD +19 -17
- {agent_audit-0.1.0.dist-info → agent_audit-0.2.0.dist-info}/WHEEL +0 -0
- {agent_audit-0.1.0.dist-info → agent_audit-0.2.0.dist-info}/entry_points.txt +0 -0
agent_audit/cli/commands/scan.py
CHANGED
|
@@ -30,6 +30,7 @@ def run_scan(
|
|
|
30
30
|
fail_on_severity: str,
|
|
31
31
|
baseline_path: Optional[Path] = None,
|
|
32
32
|
save_baseline_path: Optional[Path] = None,
|
|
33
|
+
rules_dir: Optional[Path] = None,
|
|
33
34
|
verbose: bool = False,
|
|
34
35
|
quiet: bool = False
|
|
35
36
|
) -> int:
|
|
@@ -42,7 +43,7 @@ def run_scan(
|
|
|
42
43
|
ignore_manager = IgnoreManager()
|
|
43
44
|
config_loaded = ignore_manager.load(path)
|
|
44
45
|
|
|
45
|
-
if verbose and config_loaded:
|
|
46
|
+
if verbose and config_loaded and output_format == "terminal":
|
|
46
47
|
console.print(f"[dim]Loaded config from: {ignore_manager._loaded_from}[/dim]")
|
|
47
48
|
|
|
48
49
|
# Get exclude patterns from config
|
|
@@ -58,9 +59,9 @@ def run_scan(
|
|
|
58
59
|
|
|
59
60
|
# Find rules directory - check multiple possible locations
|
|
60
61
|
possible_rules_dirs = [
|
|
61
|
-
#
|
|
62
|
-
Path(__file__).parent.parent
|
|
63
|
-
#
|
|
62
|
+
# Inside the package (installed via pip/wheel)
|
|
63
|
+
Path(__file__).parent.parent / "rules" / "builtin",
|
|
64
|
+
# Project root rules dir (development mode, symlink-installed)
|
|
64
65
|
Path(__file__).resolve().parent.parent.parent.parent.parent.parent.parent / "rules" / "builtin",
|
|
65
66
|
# Relative to current working directory
|
|
66
67
|
Path.cwd() / "rules" / "builtin",
|
|
@@ -68,59 +69,66 @@ def run_scan(
|
|
|
68
69
|
Path.cwd().parent.parent / "rules" / "builtin",
|
|
69
70
|
]
|
|
70
71
|
|
|
71
|
-
for
|
|
72
|
-
if
|
|
73
|
-
rule_engine.add_builtin_rules_dir(
|
|
72
|
+
for builtin_dir in possible_rules_dirs:
|
|
73
|
+
if builtin_dir.exists():
|
|
74
|
+
rule_engine.add_builtin_rules_dir(builtin_dir)
|
|
74
75
|
break
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
# Load custom rules if --rules-dir is specified
|
|
78
|
+
custom_rules_dirs: Optional[List[Path]] = None
|
|
79
|
+
if rules_dir and rules_dir.exists():
|
|
80
|
+
custom_rules_dirs = [rules_dir]
|
|
81
|
+
if not quiet and output_format == "terminal":
|
|
82
|
+
console.print(f"[dim]Loading custom rules from: {rules_dir}[/dim]")
|
|
83
|
+
|
|
84
|
+
rule_engine.load_rules(additional_dirs=custom_rules_dirs)
|
|
77
85
|
|
|
78
86
|
# Collect all findings
|
|
79
87
|
all_findings: List[Finding] = []
|
|
80
88
|
scanned_files = 0
|
|
81
89
|
|
|
82
90
|
# Run Python scanner
|
|
83
|
-
if not quiet:
|
|
91
|
+
if not quiet and output_format == "terminal":
|
|
84
92
|
console.print("[dim]Scanning Python files...[/dim]")
|
|
85
93
|
|
|
86
94
|
python_results = python_scanner.scan(path)
|
|
87
|
-
for
|
|
95
|
+
for py_result in python_results:
|
|
88
96
|
scanned_files += 1
|
|
89
97
|
|
|
90
98
|
# Generate findings from dangerous patterns
|
|
91
99
|
findings = rule_engine.evaluate_dangerous_patterns(
|
|
92
|
-
|
|
93
|
-
|
|
100
|
+
py_result.dangerous_patterns,
|
|
101
|
+
py_result.source_file
|
|
94
102
|
)
|
|
95
103
|
all_findings.extend(findings)
|
|
96
104
|
|
|
97
105
|
# Check for credentials in source
|
|
98
106
|
try:
|
|
99
|
-
source = Path(
|
|
100
|
-
cred_findings = rule_engine.evaluate_credentials(source,
|
|
107
|
+
source = Path(py_result.source_file).read_text(encoding='utf-8')
|
|
108
|
+
cred_findings = rule_engine.evaluate_credentials(source, py_result.source_file)
|
|
101
109
|
all_findings.extend(cred_findings)
|
|
102
110
|
except Exception:
|
|
103
111
|
pass
|
|
104
112
|
|
|
105
113
|
# Evaluate tool permissions
|
|
106
|
-
if
|
|
114
|
+
if py_result.tools:
|
|
107
115
|
perm_findings = rule_engine.evaluate_permission_scope(
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
py_result.tools,
|
|
117
|
+
py_result.source_file
|
|
110
118
|
)
|
|
111
119
|
all_findings.extend(perm_findings)
|
|
112
120
|
|
|
113
121
|
# Run MCP config scanner
|
|
114
|
-
if not quiet:
|
|
122
|
+
if not quiet and output_format == "terminal":
|
|
115
123
|
console.print("[dim]Scanning MCP configurations...[/dim]")
|
|
116
124
|
|
|
117
125
|
mcp_results = mcp_scanner.scan(path)
|
|
118
|
-
for
|
|
126
|
+
for mcp_result in mcp_results:
|
|
119
127
|
scanned_files += 1
|
|
120
128
|
|
|
121
129
|
# Convert server configs to dicts for rule engine
|
|
122
130
|
server_dicts = []
|
|
123
|
-
for server in
|
|
131
|
+
for server in mcp_result.servers:
|
|
124
132
|
server_dict = {
|
|
125
133
|
'name': server.name,
|
|
126
134
|
'url': server.url,
|
|
@@ -132,16 +140,16 @@ def run_scan(
|
|
|
132
140
|
}
|
|
133
141
|
server_dicts.append(server_dict)
|
|
134
142
|
|
|
135
|
-
mcp_findings = rule_engine.evaluate_mcp_config(server_dicts,
|
|
143
|
+
mcp_findings = rule_engine.evaluate_mcp_config(server_dicts, mcp_result.source_file)
|
|
136
144
|
all_findings.extend(mcp_findings)
|
|
137
145
|
|
|
138
146
|
# Run secret scanner
|
|
139
|
-
if not quiet:
|
|
147
|
+
if not quiet and output_format == "terminal":
|
|
140
148
|
console.print("[dim]Scanning for secrets...[/dim]")
|
|
141
149
|
|
|
142
150
|
secret_results = secret_scanner.scan(path)
|
|
143
|
-
for
|
|
144
|
-
for secret in
|
|
151
|
+
for secret_result in secret_results:
|
|
152
|
+
for secret in secret_result.secrets:
|
|
145
153
|
from agent_audit.models.risk import Location, Category
|
|
146
154
|
from agent_audit.models.finding import Remediation
|
|
147
155
|
|
|
@@ -153,7 +161,7 @@ def run_scan(
|
|
|
153
161
|
Severity.HIGH if secret.severity == "high" else Severity.MEDIUM,
|
|
154
162
|
category=Category.CREDENTIAL_EXPOSURE,
|
|
155
163
|
location=Location(
|
|
156
|
-
file_path=
|
|
164
|
+
file_path=secret_result.source_file,
|
|
157
165
|
start_line=secret.line_number,
|
|
158
166
|
end_line=secret.line_number,
|
|
159
167
|
start_column=secret.start_col,
|
|
@@ -175,7 +183,7 @@ def run_scan(
|
|
|
175
183
|
if baseline_path and baseline_path.exists():
|
|
176
184
|
baseline = load_baseline(baseline_path)
|
|
177
185
|
all_findings = filter_by_baseline(all_findings, baseline)
|
|
178
|
-
if not quiet:
|
|
186
|
+
if not quiet and output_format == "terminal":
|
|
179
187
|
console.print(f"[dim]Filtered by baseline: {baseline_path}[/dim]")
|
|
180
188
|
|
|
181
189
|
# Filter by minimum severity
|
|
@@ -191,7 +199,7 @@ def run_scan(
|
|
|
191
199
|
# Save baseline if requested
|
|
192
200
|
if save_baseline_path:
|
|
193
201
|
save_baseline(all_findings, save_baseline_path)
|
|
194
|
-
if not quiet:
|
|
202
|
+
if not quiet and output_format == "terminal":
|
|
195
203
|
console.print(f"[dim]Saved baseline to: {save_baseline_path}[/dim]")
|
|
196
204
|
|
|
197
205
|
# Output results
|
|
@@ -209,7 +217,7 @@ def run_scan(
|
|
|
209
217
|
if output_path:
|
|
210
218
|
output_path.write_text(json_output, encoding="utf-8")
|
|
211
219
|
else:
|
|
212
|
-
|
|
220
|
+
click.echo(json_output)
|
|
213
221
|
elif output_format == "sarif":
|
|
214
222
|
from agent_audit.cli.formatters.sarif import SARIFFormatter
|
|
215
223
|
formatter = SARIFFormatter()
|
|
@@ -285,6 +293,8 @@ def _output_markdown(findings: List[Finding], scan_path: str, output_path: Optio
|
|
|
285
293
|
default='low', help='Minimum severity to report')
|
|
286
294
|
@click.option('--rules', '-r', type=click.Path(exists=True),
|
|
287
295
|
multiple=True, help='Additional rule files')
|
|
296
|
+
@click.option('--rules-dir', type=click.Path(exists=True, file_okay=False),
|
|
297
|
+
help='Directory containing custom YAML rule files')
|
|
288
298
|
@click.option('--fail-on',
|
|
289
299
|
type=click.Choice(['critical', 'high', 'medium', 'low']),
|
|
290
300
|
default='high', help='Exit with error if findings at this level')
|
|
@@ -294,8 +304,8 @@ def _output_markdown(findings: List[Finding], scan_path: str, output_path: Optio
|
|
|
294
304
|
help='Save current findings as baseline')
|
|
295
305
|
@click.pass_context
|
|
296
306
|
def scan(ctx: click.Context, path: str, output_format: str, output: Optional[str],
|
|
297
|
-
severity: str, rules: tuple,
|
|
298
|
-
save_baseline: Optional[str]):
|
|
307
|
+
severity: str, rules: tuple, rules_dir: Optional[str], fail_on: str,
|
|
308
|
+
baseline: Optional[str], save_baseline: Optional[str]):
|
|
299
309
|
"""
|
|
300
310
|
Scan agent code and configurations for security issues.
|
|
301
311
|
|
|
@@ -322,6 +332,7 @@ def scan(ctx: click.Context, path: str, output_format: str, output: Optional[str
|
|
|
322
332
|
fail_on_severity=fail_on,
|
|
323
333
|
baseline_path=Path(baseline) if baseline else None,
|
|
324
334
|
save_baseline_path=Path(save_baseline) if save_baseline else None,
|
|
335
|
+
rules_dir=Path(rules_dir) if rules_dir else None,
|
|
325
336
|
verbose=ctx.obj.get('verbose', False),
|
|
326
337
|
quiet=ctx.obj.get('quiet', False)
|
|
327
338
|
)
|
|
@@ -69,12 +69,12 @@ class JSONFormatter:
|
|
|
69
69
|
actionable = sum(1 for f in findings if f.is_actionable())
|
|
70
70
|
suppressed = sum(1 for f in findings if f.suppressed)
|
|
71
71
|
|
|
72
|
-
by_severity = {}
|
|
72
|
+
by_severity: Dict[str, int] = {}
|
|
73
73
|
for f in findings:
|
|
74
74
|
sev = f.severity.value
|
|
75
75
|
by_severity[sev] = by_severity.get(sev, 0) + 1
|
|
76
76
|
|
|
77
|
-
by_category = {}
|
|
77
|
+
by_category: Dict[str, int] = {}
|
|
78
78
|
for f in findings:
|
|
79
79
|
cat = f.category.value
|
|
80
80
|
by_category[cat] = by_category.get(cat, 0) + 1
|
|
@@ -93,15 +93,21 @@ class SARIFFormatter:
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
# Add CWE/OWASP tags
|
|
96
|
-
tags = []
|
|
96
|
+
tags: List[str] = []
|
|
97
97
|
if finding.cwe_id:
|
|
98
98
|
tags.append(f"external/cwe/{finding.cwe_id.lower()}")
|
|
99
99
|
if finding.owasp_id:
|
|
100
|
-
|
|
100
|
+
# Use OWASP-Agentic prefix for ASI-XX identifiers
|
|
101
|
+
if finding.owasp_id.startswith("ASI-"):
|
|
102
|
+
tags.append(f"OWASP-Agentic-{finding.owasp_id}")
|
|
103
|
+
else:
|
|
104
|
+
tags.append(f"external/owasp/{finding.owasp_id}")
|
|
105
|
+
# Also add owasp-agentic-id to properties
|
|
106
|
+
rule["properties"]["owasp-agentic-id"] = finding.owasp_id # type: ignore[index]
|
|
101
107
|
if finding.category:
|
|
102
108
|
tags.append(finding.category.value)
|
|
103
109
|
if tags:
|
|
104
|
-
rule["properties"]["tags"] = tags
|
|
110
|
+
rule["properties"]["tags"] = tags # type: ignore[index]
|
|
105
111
|
|
|
106
112
|
rules_map[finding.rule_id] = rule
|
|
107
113
|
|
agent_audit/models/finding.py
CHANGED
|
@@ -7,6 +7,21 @@ from typing import Optional, Dict, Any
|
|
|
7
7
|
from agent_audit.models.risk import Severity, Category, Location
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
# OWASP Agentic Top 10 (2026) ID to name mapping
|
|
11
|
+
OWASP_AGENTIC_MAP: Dict[str, str] = {
|
|
12
|
+
"ASI-01": "Agent Goal Hijack",
|
|
13
|
+
"ASI-02": "Tool Misuse and Exploitation",
|
|
14
|
+
"ASI-03": "Identity and Privilege Abuse",
|
|
15
|
+
"ASI-04": "Agentic Supply Chain Vulnerabilities",
|
|
16
|
+
"ASI-05": "Unexpected Code Execution",
|
|
17
|
+
"ASI-06": "Memory and Context Poisoning",
|
|
18
|
+
"ASI-07": "Insecure Inter-Agent Communication",
|
|
19
|
+
"ASI-08": "Cascading Failures",
|
|
20
|
+
"ASI-09": "Human-Agent Trust Exploitation",
|
|
21
|
+
"ASI-10": "Rogue Agents",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
10
25
|
@dataclass
|
|
11
26
|
class Remediation:
|
|
12
27
|
"""Remediation guidance for a finding."""
|
|
@@ -77,14 +92,11 @@ class Finding:
|
|
|
77
92
|
}
|
|
78
93
|
|
|
79
94
|
# Add column information if available
|
|
95
|
+
region = result["locations"][0]["physicalLocation"]["region"] # type: ignore[index]
|
|
80
96
|
if self.location.start_column is not None:
|
|
81
|
-
|
|
82
|
-
self.location.start_column
|
|
83
|
-
)
|
|
97
|
+
region["startColumn"] = self.location.start_column
|
|
84
98
|
if self.location.end_column is not None:
|
|
85
|
-
|
|
86
|
-
self.location.end_column
|
|
87
|
-
)
|
|
99
|
+
region["endColumn"] = self.location.end_column
|
|
88
100
|
|
|
89
101
|
# Add fingerprint for deduplication
|
|
90
102
|
result["fingerprints"] = {
|
|
@@ -92,7 +104,7 @@ class Finding:
|
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
# Add properties for additional metadata
|
|
95
|
-
properties = {}
|
|
107
|
+
properties: Dict[str, Any] = {}
|
|
96
108
|
if self.confidence < 1.0:
|
|
97
109
|
properties["confidence"] = self.confidence
|
|
98
110
|
if self.cwe_id:
|
agent_audit/models/risk.py
CHANGED
|
@@ -34,6 +34,7 @@ class Severity(Enum):
|
|
|
34
34
|
|
|
35
35
|
class Category(Enum):
|
|
36
36
|
"""Categories for security findings."""
|
|
37
|
+
# Original categories
|
|
37
38
|
COMMAND_INJECTION = "command_injection"
|
|
38
39
|
DATA_EXFILTRATION = "data_exfiltration"
|
|
39
40
|
PRIVILEGE_ESCALATION = "privilege_escalation"
|
|
@@ -42,6 +43,18 @@ class Category(Enum):
|
|
|
42
43
|
PROMPT_INJECTION = "prompt_injection"
|
|
43
44
|
EXCESSIVE_PERMISSION = "excessive_permission"
|
|
44
45
|
|
|
46
|
+
# OWASP Agentic Top 10 (2026) extended categories
|
|
47
|
+
GOAL_HIJACK = "goal_hijack" # ASI-01
|
|
48
|
+
TOOL_MISUSE = "tool_misuse" # ASI-02
|
|
49
|
+
IDENTITY_PRIVILEGE_ABUSE = "identity_privilege_abuse" # ASI-03
|
|
50
|
+
SUPPLY_CHAIN_AGENTIC = "supply_chain_agentic" # ASI-04
|
|
51
|
+
UNEXPECTED_CODE_EXECUTION = "unexpected_code_execution" # ASI-05
|
|
52
|
+
MEMORY_POISONING = "memory_poisoning" # ASI-06
|
|
53
|
+
INSECURE_INTER_AGENT_COMM = "insecure_inter_agent_comm" # ASI-07
|
|
54
|
+
CASCADING_FAILURES = "cascading_failures" # ASI-08
|
|
55
|
+
TRUST_EXPLOITATION = "trust_exploitation" # ASI-09
|
|
56
|
+
ROGUE_AGENT = "rogue_agent" # ASI-10
|
|
57
|
+
|
|
45
58
|
|
|
46
59
|
@dataclass
|
|
47
60
|
class Location:
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# OWASP Agentic Security Rules
|
|
2
|
+
# Core rules for agent security scanning
|
|
3
|
+
|
|
4
|
+
rules:
|
|
5
|
+
- id: AGENT-001
|
|
6
|
+
title: Command Injection via Unsanitized Input
|
|
7
|
+
description: Tool accepts user input passed directly to shell execution without proper sanitization.
|
|
8
|
+
severity: critical
|
|
9
|
+
category: command_injection
|
|
10
|
+
cwe_id: CWE-78
|
|
11
|
+
owasp_id: OWASP-AGENT-02
|
|
12
|
+
|
|
13
|
+
detection:
|
|
14
|
+
patterns:
|
|
15
|
+
- type: python_ast
|
|
16
|
+
match:
|
|
17
|
+
- "subprocess.run"
|
|
18
|
+
- "subprocess.Popen"
|
|
19
|
+
- "subprocess.call"
|
|
20
|
+
- "os.system"
|
|
21
|
+
- "os.popen"
|
|
22
|
+
- "eval"
|
|
23
|
+
- "exec"
|
|
24
|
+
|
|
25
|
+
- type: function_call
|
|
26
|
+
functions:
|
|
27
|
+
- subprocess.run
|
|
28
|
+
- subprocess.Popen
|
|
29
|
+
- subprocess.call
|
|
30
|
+
arguments:
|
|
31
|
+
shell: true
|
|
32
|
+
|
|
33
|
+
remediation:
|
|
34
|
+
description: Use shlex.quote() to escape user input, use argument lists instead of shell strings, implement a command allowlist.
|
|
35
|
+
code_example: |
|
|
36
|
+
# Safe: use argument list
|
|
37
|
+
subprocess.run(["ls", shlex.quote(user_input)])
|
|
38
|
+
references:
|
|
39
|
+
- https://owasp.org/www-community/attacks/Command_Injection
|
|
40
|
+
|
|
41
|
+
- id: AGENT-002
|
|
42
|
+
title: Excessive Agent Permissions
|
|
43
|
+
description: Agent is configured with more permissions than necessary for its intended purpose.
|
|
44
|
+
severity: medium
|
|
45
|
+
category: excessive_permission
|
|
46
|
+
cwe_id: CWE-250
|
|
47
|
+
owasp_id: OWASP-AGENT-01
|
|
48
|
+
|
|
49
|
+
detection:
|
|
50
|
+
conditions:
|
|
51
|
+
tool_count_threshold: 15
|
|
52
|
+
high_risk_permission_threshold: 5
|
|
53
|
+
|
|
54
|
+
remediation:
|
|
55
|
+
description: Apply principle of least privilege, split into specialized agents, require approval for high-risk operations.
|
|
56
|
+
references:
|
|
57
|
+
- https://owasp.org/www-community/vulnerabilities/Least_Privilege_Violation
|
|
58
|
+
|
|
59
|
+
- id: AGENT-003
|
|
60
|
+
title: Potential Data Exfiltration Chain
|
|
61
|
+
description: Agent has access to both sensitive data sources and external network capabilities.
|
|
62
|
+
severity: high
|
|
63
|
+
category: data_exfiltration
|
|
64
|
+
cwe_id: CWE-200
|
|
65
|
+
owasp_id: OWASP-AGENT-05
|
|
66
|
+
|
|
67
|
+
detection:
|
|
68
|
+
operation_chain:
|
|
69
|
+
source_permissions:
|
|
70
|
+
- SECRET_ACCESS
|
|
71
|
+
- FILE_READ
|
|
72
|
+
- DATABASE_READ
|
|
73
|
+
target_permissions:
|
|
74
|
+
- NETWORK_OUTBOUND
|
|
75
|
+
|
|
76
|
+
remediation:
|
|
77
|
+
description: Implement network egress allowlist, add approval workflow for sensitive operations.
|
|
78
|
+
references:
|
|
79
|
+
- https://owasp.org/www-community/attacks/Data_Exfiltration
|
|
80
|
+
|
|
81
|
+
- id: AGENT-004
|
|
82
|
+
title: Hardcoded Credentials in Agent Config
|
|
83
|
+
description: Agent configuration contains hardcoded API keys, passwords, or other secrets.
|
|
84
|
+
severity: critical
|
|
85
|
+
category: credential_exposure
|
|
86
|
+
cwe_id: CWE-798
|
|
87
|
+
owasp_id: OWASP-AGENT-03
|
|
88
|
+
|
|
89
|
+
detection:
|
|
90
|
+
patterns:
|
|
91
|
+
- type: regex
|
|
92
|
+
patterns:
|
|
93
|
+
- "AKIA[0-9A-Z]{16}"
|
|
94
|
+
- "sk-[a-zA-Z0-9]{48,}"
|
|
95
|
+
- "sk-ant-[a-zA-Z0-9-]{40,}"
|
|
96
|
+
- "ghp_[a-zA-Z0-9]{36}"
|
|
97
|
+
- "gho_[a-zA-Z0-9]{36}"
|
|
98
|
+
|
|
99
|
+
remediation:
|
|
100
|
+
description: Use environment variables or a secrets manager instead of hardcoded credentials.
|
|
101
|
+
code_example: |
|
|
102
|
+
# Safe: use environment variable
|
|
103
|
+
api_key = os.environ.get("OPENAI_API_KEY")
|
|
104
|
+
references:
|
|
105
|
+
- https://cwe.mitre.org/data/definitions/798.html
|
|
106
|
+
|
|
107
|
+
- id: AGENT-005
|
|
108
|
+
title: Unverified MCP Server
|
|
109
|
+
description: Agent connects to an MCP server that lacks signature verification.
|
|
110
|
+
severity: high
|
|
111
|
+
category: supply_chain
|
|
112
|
+
cwe_id: CWE-494
|
|
113
|
+
owasp_id: OWASP-AGENT-04
|
|
114
|
+
|
|
115
|
+
detection:
|
|
116
|
+
mcp_server:
|
|
117
|
+
verified: false
|
|
118
|
+
trusted_sources:
|
|
119
|
+
- "docker.io/mcp-catalog/"
|
|
120
|
+
- "ghcr.io/anthropics/"
|
|
121
|
+
- "ghcr.io/modelcontextprotocol/"
|
|
122
|
+
|
|
123
|
+
remediation:
|
|
124
|
+
description: Only use MCP servers from trusted registries, enable signature verification.
|
|
125
|
+
references:
|
|
126
|
+
- https://modelcontextprotocol.io/docs/security
|