agent-audit 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.
Files changed (37) hide show
  1. agent_audit/__init__.py +3 -0
  2. agent_audit/__main__.py +13 -0
  3. agent_audit/cli/__init__.py +1 -0
  4. agent_audit/cli/commands/__init__.py +1 -0
  5. agent_audit/cli/commands/init.py +44 -0
  6. agent_audit/cli/commands/inspect.py +236 -0
  7. agent_audit/cli/commands/scan.py +329 -0
  8. agent_audit/cli/formatters/__init__.py +1 -0
  9. agent_audit/cli/formatters/json.py +138 -0
  10. agent_audit/cli/formatters/sarif.py +155 -0
  11. agent_audit/cli/formatters/terminal.py +221 -0
  12. agent_audit/cli/main.py +34 -0
  13. agent_audit/config/__init__.py +1 -0
  14. agent_audit/config/ignore.py +477 -0
  15. agent_audit/core_utils/__init__.py +1 -0
  16. agent_audit/models/__init__.py +18 -0
  17. agent_audit/models/finding.py +159 -0
  18. agent_audit/models/risk.py +77 -0
  19. agent_audit/models/tool.py +182 -0
  20. agent_audit/rules/__init__.py +6 -0
  21. agent_audit/rules/engine.py +503 -0
  22. agent_audit/rules/loader.py +160 -0
  23. agent_audit/scanners/__init__.py +5 -0
  24. agent_audit/scanners/base.py +32 -0
  25. agent_audit/scanners/config_scanner.py +390 -0
  26. agent_audit/scanners/mcp_config_scanner.py +321 -0
  27. agent_audit/scanners/mcp_inspector.py +421 -0
  28. agent_audit/scanners/python_scanner.py +544 -0
  29. agent_audit/scanners/secret_scanner.py +521 -0
  30. agent_audit/utils/__init__.py +21 -0
  31. agent_audit/utils/compat.py +98 -0
  32. agent_audit/utils/mcp_client.py +343 -0
  33. agent_audit/version.py +3 -0
  34. agent_audit-0.1.0.dist-info/METADATA +219 -0
  35. agent_audit-0.1.0.dist-info/RECORD +37 -0
  36. agent_audit-0.1.0.dist-info/WHEEL +4 -0
  37. agent_audit-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,3 @@
1
+ """Agent Audit - Security scanner for AI agents and MCP configurations."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,13 @@
1
+ """Entry point for running agent-audit as a module."""
2
+
3
+ import sys
4
+
5
+ # Windows event loop policy fix - must be set before any asyncio imports
6
+ if sys.platform == "win32":
7
+ import asyncio
8
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
9
+
10
+ from agent_audit.cli.main import cli
11
+
12
+ if __name__ == "__main__":
13
+ cli()
@@ -0,0 +1 @@
1
+ """CLI module for agent-audit."""
@@ -0,0 +1 @@
1
+ """CLI commands for agent-audit."""
@@ -0,0 +1,44 @@
1
+ """Init command for creating configuration files."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ from agent_audit.config.ignore import create_default_config
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.command()
15
+ @click.option('--force', '-f', is_flag=True, help='Overwrite existing config')
16
+ def init(force: bool):
17
+ """
18
+ Initialize agent-audit configuration.
19
+
20
+ Creates a .agent-audit.yaml file in the current directory with
21
+ default settings and example ignore rules.
22
+
23
+ Examples:
24
+
25
+ agent-audit init
26
+
27
+ agent-audit init --force
28
+ """
29
+ config_path = Path('.agent-audit.yaml')
30
+
31
+ if config_path.exists() and not force:
32
+ console.print(f"[yellow]Configuration file already exists: {config_path}[/yellow]")
33
+ console.print("Use --force to overwrite")
34
+ sys.exit(1)
35
+
36
+ config_content = create_default_config()
37
+ config_path.write_text(config_content, encoding="utf-8")
38
+
39
+ console.print(f"[green]Created configuration file: {config_path}[/green]")
40
+ console.print()
41
+ console.print("Edit this file to:")
42
+ console.print(" - Add allowed hosts for network destinations")
43
+ console.print(" - Configure ignore rules for false positives")
44
+ console.print(" - Set scan exclusion patterns")
@@ -0,0 +1,236 @@
1
+ """Inspect command for probing MCP servers."""
2
+
3
+ import asyncio
4
+ import sys
5
+ from typing import Optional
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+ from rich.panel import Panel
11
+
12
+ from agent_audit.scanners.mcp_inspector import MCPInspector, MCPInspectionResult
13
+ from agent_audit.utils.mcp_client import TransportType
14
+
15
+ console = Console()
16
+
17
+
18
+ def render_inspection_result(result: MCPInspectionResult, output_format: str = "terminal"):
19
+ """Render inspection result to the console."""
20
+ if output_format == "json":
21
+ _render_json(result)
22
+ else:
23
+ _render_terminal(result)
24
+
25
+
26
+ def _render_terminal(result: MCPInspectionResult):
27
+ """Render result as Rich terminal output."""
28
+ # Status indicator
29
+ if result.connected:
30
+ status = "[green]✓ Connected[/green]"
31
+ border_style = "blue" if result.risk_score < 5 else "red"
32
+ else:
33
+ status = "[red]✗ Failed[/red]"
34
+ border_style = "red"
35
+
36
+ # Header panel
37
+ header_text = (
38
+ f"[bold]MCP Server Inspection[/bold]\n"
39
+ f"Server: {result.server_name}"
40
+ )
41
+ if result.server_version:
42
+ header_text += f" v{result.server_version}"
43
+ header_text += "\n"
44
+ header_text += f"Status: {status} | Response: {result.response_time_ms:.0f}ms\n"
45
+ header_text += f"Risk Score: {result.risk_score:.1f}/10"
46
+
47
+ console.print(Panel.fit(header_text, border_style=border_style))
48
+
49
+ if not result.connected:
50
+ console.print(f"[red]Error: {result.connection_error}[/red]")
51
+ return
52
+
53
+ # Capabilities
54
+ if result.capabilities_declared:
55
+ caps = ", ".join(result.capabilities_declared) or "none"
56
+ console.print(f"\n[dim]Capabilities:[/dim] {caps}")
57
+
58
+ # Tools table
59
+ console.print(f"\n[bold]Tools ({result.tool_count})[/bold]")
60
+
61
+ if result.tools:
62
+ tool_table = Table(show_header=True, header_style="bold cyan")
63
+ tool_table.add_column("Tool", style="cyan")
64
+ tool_table.add_column("Permissions", style="yellow")
65
+ tool_table.add_column("Risk", justify="center")
66
+ tool_table.add_column("Validation")
67
+
68
+ risk_emoji = {
69
+ 1: "🟢", # SAFE
70
+ 2: "🟢", # LOW
71
+ 3: "🟡", # MEDIUM
72
+ 4: "🟠", # HIGH
73
+ 5: "🔴", # CRITICAL
74
+ }
75
+
76
+ for tool in result.tools:
77
+ perms = ", ".join(p.name for p in tool.permissions) or "none"
78
+ risk_value = tool.risk_level.value if hasattr(tool.risk_level, 'value') else 1
79
+ risk = risk_emoji.get(risk_value, "⚪")
80
+ validation = "✅" if tool.has_input_validation else "❌"
81
+
82
+ # Truncate description if needed
83
+ tool_name = tool.name
84
+ if len(tool_name) > 30:
85
+ tool_name = tool_name[:27] + "..."
86
+
87
+ tool_table.add_row(tool_name, perms, risk, validation)
88
+
89
+ console.print(tool_table)
90
+ else:
91
+ console.print("[dim]No tools exposed[/dim]")
92
+
93
+ # Resources
94
+ if result.resources:
95
+ console.print(f"\n[bold]Resources ({result.resource_count})[/bold]")
96
+ for res in result.resources[:10]: # Limit display
97
+ uri = res.get('uri', 'unknown')
98
+ console.print(f" 📄 {uri}")
99
+ if result.resource_count > 10:
100
+ console.print(f" [dim]... and {result.resource_count - 10} more[/dim]")
101
+
102
+ # Prompts
103
+ if result.prompts:
104
+ console.print(f"\n[bold]Prompts ({result.prompt_count})[/bold]")
105
+ for prompt in result.prompts[:10]:
106
+ name = prompt.get('name', 'unknown')
107
+ console.print(f" 💬 {name}")
108
+ if result.prompt_count > 10:
109
+ console.print(f" [dim]... and {result.prompt_count - 10} more[/dim]")
110
+
111
+ # Security findings
112
+ if result.findings:
113
+ console.print(f"\n[bold red]Security Findings ({len(result.findings)})[/bold red]")
114
+
115
+ severity_colors = {
116
+ "critical": "red",
117
+ "high": "red",
118
+ "medium": "yellow",
119
+ "low": "blue"
120
+ }
121
+
122
+ for finding in result.findings:
123
+ severity = finding.get("severity", "medium")
124
+ color = severity_colors.get(severity, "white")
125
+ desc = finding.get("description", "Unknown issue")
126
+ tool_name = finding.get("tool", "")
127
+
128
+ if tool_name:
129
+ console.print(f" [{color}]⚠ {severity.upper()}[/{color}]: {desc} (tool: {tool_name})")
130
+ else:
131
+ console.print(f" [{color}]⚠ {severity.upper()}[/{color}]: {desc}")
132
+
133
+
134
+ def _render_json(result: MCPInspectionResult):
135
+ """Render result as JSON."""
136
+ import json
137
+
138
+ output = {
139
+ "server_name": result.server_name,
140
+ "server_version": result.server_version,
141
+ "transport": result.transport.value,
142
+ "connected": result.connected,
143
+ "connection_error": result.connection_error,
144
+ "response_time_ms": result.response_time_ms,
145
+ "risk_score": result.risk_score,
146
+ "capabilities": result.capabilities_declared,
147
+ "tool_count": result.tool_count,
148
+ "tools": [t.to_dict() for t in result.tools],
149
+ "resource_count": result.resource_count,
150
+ "resources": result.resources,
151
+ "prompt_count": result.prompt_count,
152
+ "prompts": result.prompts,
153
+ "findings": result.findings,
154
+ }
155
+
156
+ console.print_json(json.dumps(output, indent=2))
157
+
158
+
159
+ async def run_inspect_async(
160
+ target: str,
161
+ transport: Optional[str],
162
+ timeout: int,
163
+ output_format: str
164
+ ) -> int:
165
+ """Run the inspection asynchronously."""
166
+ inspector = MCPInspector(timeout=timeout)
167
+
168
+ # Determine transport type
169
+ transport_type: Optional[TransportType] = None
170
+ if transport:
171
+ transport_type = TransportType(transport)
172
+
173
+ result = await inspector.inspect(target, transport_type)
174
+ render_inspection_result(result, output_format)
175
+
176
+ # Return exit code based on risk
177
+ if not result.connected:
178
+ return 2 # Connection failure
179
+ elif result.risk_score >= 7.0:
180
+ return 1 # High risk
181
+ return 0
182
+
183
+
184
+ def run_inspect(
185
+ target: str,
186
+ transport: Optional[str],
187
+ timeout: int,
188
+ output_format: str
189
+ ) -> int:
190
+ """Run the inspection."""
191
+ return asyncio.run(run_inspect_async(target, transport, timeout, output_format))
192
+
193
+
194
+ @click.command()
195
+ @click.argument('transport_type', type=click.Choice(['stdio', 'sse']), required=True)
196
+ @click.argument('target', nargs=-1, required=True)
197
+ @click.option('--timeout', '-t', default=30, help='Connection timeout in seconds')
198
+ @click.option('--format', '-f', 'output_format',
199
+ type=click.Choice(['terminal', 'json']), default='terminal',
200
+ help='Output format')
201
+ def inspect(transport_type: str, target: tuple, timeout: int, output_format: str):
202
+ """
203
+ Inspect a running MCP server and analyze its tools.
204
+
205
+ TRANSPORT_TYPE is either 'stdio' or 'sse'.
206
+
207
+ For stdio, TARGET is the command to run the server:
208
+
209
+ agent-audit inspect stdio -- python my_mcp_server.py
210
+
211
+ For sse, TARGET is the URL:
212
+
213
+ agent-audit inspect sse https://example.com/sse
214
+
215
+ The inspector connects to the server, retrieves its tool definitions,
216
+ and analyzes them for security risks WITHOUT executing any tools.
217
+ """
218
+ # Join target parts back together
219
+ target_str = ' '.join(target)
220
+
221
+ # Handle the "--" separator for stdio
222
+ if target_str.startswith('-- '):
223
+ target_str = target_str[3:]
224
+
225
+ if not target_str:
226
+ console.print("[red]Error: No target specified[/red]")
227
+ sys.exit(1)
228
+
229
+ exit_code = run_inspect(
230
+ target=target_str,
231
+ transport=transport_type,
232
+ timeout=timeout,
233
+ output_format=output_format
234
+ )
235
+
236
+ sys.exit(exit_code)
@@ -0,0 +1,329 @@
1
+ """Scan command implementation."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, List
5
+
6
+ import click
7
+ from rich.console import Console
8
+
9
+ from agent_audit.models.finding import Finding
10
+ from agent_audit.models.risk import Severity
11
+ from agent_audit.rules.engine import RuleEngine
12
+
13
+ from agent_audit.scanners.python_scanner import PythonScanner
14
+ from agent_audit.scanners.mcp_config_scanner import MCPConfigScanner
15
+ from agent_audit.scanners.secret_scanner import SecretScanner
16
+ from agent_audit.config.ignore import (
17
+ IgnoreManager, load_baseline, filter_by_baseline, save_baseline
18
+ )
19
+ from agent_audit.cli.formatters.terminal import format_scan_results
20
+
21
+ console = Console()
22
+
23
+
24
+ def run_scan(
25
+ path: Path,
26
+ output_format: str,
27
+ output_path: Optional[Path],
28
+ min_severity: str,
29
+ additional_rules: List[str],
30
+ fail_on_severity: str,
31
+ baseline_path: Optional[Path] = None,
32
+ save_baseline_path: Optional[Path] = None,
33
+ verbose: bool = False,
34
+ quiet: bool = False
35
+ ) -> int:
36
+ """
37
+ Run the security scan.
38
+
39
+ Returns exit code: 0 for success, 1 for findings at fail_on level.
40
+ """
41
+ # Initialize ignore manager first to get exclude patterns
42
+ ignore_manager = IgnoreManager()
43
+ config_loaded = ignore_manager.load(path)
44
+
45
+ if verbose and config_loaded:
46
+ console.print(f"[dim]Loaded config from: {ignore_manager._loaded_from}[/dim]")
47
+
48
+ # Get exclude patterns from config
49
+ exclude_patterns = ignore_manager.get_exclude_patterns()
50
+
51
+ # Initialize scanners with exclude patterns
52
+ python_scanner = PythonScanner(exclude_patterns=exclude_patterns)
53
+ mcp_scanner = MCPConfigScanner()
54
+ secret_scanner = SecretScanner(exclude_paths=exclude_patterns)
55
+
56
+ # Initialize rule engine
57
+ rule_engine = RuleEngine()
58
+
59
+ # Find rules directory - check multiple possible locations
60
+ 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)
64
+ Path(__file__).resolve().parent.parent.parent.parent.parent.parent.parent / "rules" / "builtin",
65
+ # Relative to current working directory
66
+ Path.cwd() / "rules" / "builtin",
67
+ # Go up from cwd if in packages/audit
68
+ Path.cwd().parent.parent / "rules" / "builtin",
69
+ ]
70
+
71
+ for rules_dir in possible_rules_dirs:
72
+ if rules_dir.exists():
73
+ rule_engine.add_builtin_rules_dir(rules_dir)
74
+ break
75
+
76
+ rule_engine.load_rules()
77
+
78
+ # Collect all findings
79
+ all_findings: List[Finding] = []
80
+ scanned_files = 0
81
+
82
+ # Run Python scanner
83
+ if not quiet:
84
+ console.print("[dim]Scanning Python files...[/dim]")
85
+
86
+ python_results = python_scanner.scan(path)
87
+ for result in python_results:
88
+ scanned_files += 1
89
+
90
+ # Generate findings from dangerous patterns
91
+ findings = rule_engine.evaluate_dangerous_patterns(
92
+ result.dangerous_patterns,
93
+ result.source_file
94
+ )
95
+ all_findings.extend(findings)
96
+
97
+ # Check for credentials in source
98
+ try:
99
+ source = Path(result.source_file).read_text(encoding='utf-8')
100
+ cred_findings = rule_engine.evaluate_credentials(source, result.source_file)
101
+ all_findings.extend(cred_findings)
102
+ except Exception:
103
+ pass
104
+
105
+ # Evaluate tool permissions
106
+ if result.tools:
107
+ perm_findings = rule_engine.evaluate_permission_scope(
108
+ result.tools,
109
+ result.source_file
110
+ )
111
+ all_findings.extend(perm_findings)
112
+
113
+ # Run MCP config scanner
114
+ if not quiet:
115
+ console.print("[dim]Scanning MCP configurations...[/dim]")
116
+
117
+ mcp_results = mcp_scanner.scan(path)
118
+ for result in mcp_results:
119
+ scanned_files += 1
120
+
121
+ # Convert server configs to dicts for rule engine
122
+ server_dicts = []
123
+ for server in result.servers:
124
+ server_dict = {
125
+ 'name': server.name,
126
+ 'url': server.url,
127
+ 'command': server.command,
128
+ 'args': server.args,
129
+ 'env': server.env,
130
+ 'verified': server.verified,
131
+ '_line': server._line,
132
+ }
133
+ server_dicts.append(server_dict)
134
+
135
+ mcp_findings = rule_engine.evaluate_mcp_config(server_dicts, result.source_file)
136
+ all_findings.extend(mcp_findings)
137
+
138
+ # Run secret scanner
139
+ if not quiet:
140
+ console.print("[dim]Scanning for secrets...[/dim]")
141
+
142
+ secret_results = secret_scanner.scan(path)
143
+ for result in secret_results:
144
+ for secret in result.secrets:
145
+ from agent_audit.models.risk import Location, Category
146
+ from agent_audit.models.finding import Remediation
147
+
148
+ finding = Finding(
149
+ rule_id="AGENT-004",
150
+ title="Hardcoded Credentials",
151
+ description=f"Found {secret.pattern_name}",
152
+ severity=Severity.CRITICAL if secret.severity == "critical" else
153
+ Severity.HIGH if secret.severity == "high" else Severity.MEDIUM,
154
+ category=Category.CREDENTIAL_EXPOSURE,
155
+ location=Location(
156
+ file_path=result.source_file,
157
+ start_line=secret.line_number,
158
+ end_line=secret.line_number,
159
+ start_column=secret.start_col,
160
+ end_column=secret.end_col,
161
+ snippet=secret.line_content
162
+ ),
163
+ cwe_id="CWE-798",
164
+ remediation=Remediation(
165
+ description="Use environment variables or a secrets manager"
166
+ )
167
+ )
168
+ all_findings.append(finding)
169
+
170
+ # Apply ignore rules
171
+ for finding in all_findings:
172
+ ignore_manager.apply_to_finding(finding)
173
+
174
+ # Filter by baseline if provided
175
+ if baseline_path and baseline_path.exists():
176
+ baseline = load_baseline(baseline_path)
177
+ all_findings = filter_by_baseline(all_findings, baseline)
178
+ if not quiet:
179
+ console.print(f"[dim]Filtered by baseline: {baseline_path}[/dim]")
180
+
181
+ # Filter by minimum severity
182
+ severity_order = {
183
+ 'info': 0, 'low': 1, 'medium': 2, 'high': 3, 'critical': 4
184
+ }
185
+ min_sev_value = severity_order.get(min_severity.lower(), 0)
186
+ all_findings = [
187
+ f for f in all_findings
188
+ if severity_order.get(f.severity.value, 0) >= min_sev_value
189
+ ]
190
+
191
+ # Save baseline if requested
192
+ if save_baseline_path:
193
+ save_baseline(all_findings, save_baseline_path)
194
+ if not quiet:
195
+ console.print(f"[dim]Saved baseline to: {save_baseline_path}[/dim]")
196
+
197
+ # Output results
198
+ if output_format == "terminal":
199
+ format_scan_results(
200
+ all_findings,
201
+ str(path),
202
+ scanned_files,
203
+ verbose=verbose,
204
+ quiet=quiet
205
+ )
206
+ elif output_format == "json":
207
+ from agent_audit.cli.formatters.json import format_json
208
+ json_output = format_json(all_findings, str(path), scanned_files)
209
+ if output_path:
210
+ output_path.write_text(json_output, encoding="utf-8")
211
+ else:
212
+ console.print(json_output)
213
+ elif output_format == "sarif":
214
+ from agent_audit.cli.formatters.sarif import SARIFFormatter
215
+ formatter = SARIFFormatter()
216
+ if output_path:
217
+ formatter.save(all_findings, output_path)
218
+ if not quiet:
219
+ console.print(f"[dim]SARIF output saved to: {output_path}[/dim]")
220
+ else:
221
+ console.print(formatter.format_to_string(all_findings))
222
+ elif output_format == "markdown":
223
+ _output_markdown(all_findings, str(path), output_path)
224
+
225
+ # Determine exit code based on fail_on severity
226
+ fail_sev_value = severity_order.get(fail_on_severity.lower(), 3) # Default: high
227
+ actionable_findings = [f for f in all_findings if f.is_actionable()]
228
+ max_severity = max(
229
+ (severity_order.get(f.severity.value, 0) for f in actionable_findings),
230
+ default=0
231
+ )
232
+
233
+ if max_severity >= fail_sev_value:
234
+ return 1
235
+ return 0
236
+
237
+
238
+ def _output_markdown(findings: List[Finding], scan_path: str, output_path: Optional[Path]):
239
+ """Output findings as Markdown."""
240
+ lines = [
241
+ "# Agent Audit Security Report",
242
+ "",
243
+ f"**Scanned:** `{scan_path}`",
244
+ f"**Findings:** {len(findings)}",
245
+ "",
246
+ "## Findings",
247
+ "",
248
+ ]
249
+
250
+ for finding in findings:
251
+ sev = finding.severity.value.upper()
252
+ lines.append(f"### [{sev}] {finding.rule_id}: {finding.title}")
253
+ lines.append("")
254
+ lines.append(f"**Location:** `{finding.location.file_path}:{finding.location.start_line}`")
255
+ lines.append("")
256
+ if finding.location.snippet:
257
+ lines.append("```python")
258
+ lines.append(finding.location.snippet)
259
+ lines.append("```")
260
+ lines.append("")
261
+ lines.append(finding.description)
262
+ lines.append("")
263
+ if finding.remediation:
264
+ lines.append(f"**Fix:** {finding.remediation.description}")
265
+ lines.append("")
266
+ lines.append("---")
267
+ lines.append("")
268
+
269
+ md_content = "\n".join(lines)
270
+
271
+ if output_path:
272
+ output_path.write_text(md_content, encoding="utf-8")
273
+ else:
274
+ console.print(md_content)
275
+
276
+
277
+ @click.command()
278
+ @click.argument('path', type=click.Path(exists=True), default='.')
279
+ @click.option('--format', '-f', 'output_format',
280
+ type=click.Choice(['terminal', 'json', 'sarif', 'markdown']),
281
+ default='terminal', help='Output format')
282
+ @click.option('--output', '-o', type=click.Path(), help='Output file path')
283
+ @click.option('--severity', '-s',
284
+ type=click.Choice(['critical', 'high', 'medium', 'low', 'info']),
285
+ default='low', help='Minimum severity to report')
286
+ @click.option('--rules', '-r', type=click.Path(exists=True),
287
+ multiple=True, help='Additional rule files')
288
+ @click.option('--fail-on',
289
+ type=click.Choice(['critical', 'high', 'medium', 'low']),
290
+ default='high', help='Exit with error if findings at this level')
291
+ @click.option('--baseline', type=click.Path(),
292
+ help='Baseline file - only report new findings')
293
+ @click.option('--save-baseline', type=click.Path(),
294
+ help='Save current findings as baseline')
295
+ @click.pass_context
296
+ 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]):
299
+ """
300
+ Scan agent code and configurations for security issues.
301
+
302
+ PATH is the directory or file to scan. Defaults to current directory.
303
+
304
+ Examples:
305
+
306
+ agent-audit scan ./my-agent
307
+
308
+ agent-audit scan . --format sarif --output results.sarif
309
+
310
+ agent-audit scan . --severity critical --fail-on critical
311
+
312
+ agent-audit scan . --baseline baseline.json
313
+
314
+ agent-audit scan . --save-baseline baseline.json
315
+ """
316
+ exit_code = run_scan(
317
+ path=Path(path),
318
+ output_format=output_format,
319
+ output_path=Path(output) if output else None,
320
+ min_severity=severity,
321
+ additional_rules=list(rules),
322
+ fail_on_severity=fail_on,
323
+ baseline_path=Path(baseline) if baseline else None,
324
+ save_baseline_path=Path(save_baseline) if save_baseline else None,
325
+ verbose=ctx.obj.get('verbose', False),
326
+ quiet=ctx.obj.get('quiet', False)
327
+ )
328
+
329
+ ctx.exit(exit_code)
@@ -0,0 +1 @@
1
+ """Output formatters for agent-audit."""