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.
@@ -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
- # Relative to this file (installed package)
62
- Path(__file__).parent.parent.parent.parent.parent.parent.parent / "rules" / "builtin",
63
- # Relative to project root (development)
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 rules_dir in possible_rules_dirs:
72
- if rules_dir.exists():
73
- rule_engine.add_builtin_rules_dir(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
- rule_engine.load_rules()
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 result in python_results:
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
- result.dangerous_patterns,
93
- result.source_file
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(result.source_file).read_text(encoding='utf-8')
100
- cred_findings = rule_engine.evaluate_credentials(source, result.source_file)
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 result.tools:
114
+ if py_result.tools:
107
115
  perm_findings = rule_engine.evaluate_permission_scope(
108
- result.tools,
109
- result.source_file
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 result in mcp_results:
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 result.servers:
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, result.source_file)
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 result in secret_results:
144
- for secret in result.secrets:
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=result.source_file,
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
- console.print(json_output)
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, fail_on: str, baseline: Optional[str],
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
- tags.append(f"external/owasp/{finding.owasp_id}")
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
 
@@ -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
- result["locations"][0]["physicalLocation"]["region"]["startColumn"] = (
82
- self.location.start_column
83
- )
97
+ region["startColumn"] = self.location.start_column
84
98
  if self.location.end_column is not None:
85
- result["locations"][0]["physicalLocation"]["region"]["endColumn"] = (
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:
@@ -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