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,138 @@
1
+ """JSON output formatter."""
2
+
3
+ import json
4
+ from typing import List, Dict, Any
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+
8
+ from agent_audit.models.finding import Finding
9
+ from agent_audit.version import __version__
10
+
11
+
12
+ class JSONFormatter:
13
+ """JSON output formatter for scan results."""
14
+
15
+ def __init__(self, pretty: bool = True):
16
+ self.pretty = pretty
17
+
18
+ def format(
19
+ self,
20
+ findings: List[Finding],
21
+ scan_path: str = "",
22
+ scanned_files: int = 0
23
+ ) -> Dict[str, Any]:
24
+ """
25
+ Format findings as JSON.
26
+
27
+ Args:
28
+ findings: List of findings to format
29
+ scan_path: Path that was scanned
30
+ scanned_files: Number of files scanned
31
+
32
+ Returns:
33
+ JSON-serializable dictionary
34
+ """
35
+ return {
36
+ "version": __version__,
37
+ "scan_timestamp": datetime.utcnow().isoformat(),
38
+ "scan_path": scan_path,
39
+ "scanned_files": scanned_files,
40
+ "summary": self._create_summary(findings),
41
+ "findings": [self._finding_to_dict(f) for f in findings],
42
+ }
43
+
44
+ def format_to_string(
45
+ self,
46
+ findings: List[Finding],
47
+ scan_path: str = "",
48
+ scanned_files: int = 0
49
+ ) -> str:
50
+ """Format findings as JSON string."""
51
+ data = self.format(findings, scan_path, scanned_files)
52
+ indent = 2 if self.pretty else None
53
+ return json.dumps(data, indent=indent, default=str)
54
+
55
+ def save(
56
+ self,
57
+ findings: List[Finding],
58
+ output_path: Path,
59
+ scan_path: str = "",
60
+ scanned_files: int = 0
61
+ ):
62
+ """Save findings as JSON file."""
63
+ json_str = self.format_to_string(findings, scan_path, scanned_files)
64
+ output_path.write_text(json_str, encoding="utf-8")
65
+
66
+ def _create_summary(self, findings: List[Finding]) -> Dict[str, Any]:
67
+ """Create summary statistics."""
68
+ total = len(findings)
69
+ actionable = sum(1 for f in findings if f.is_actionable())
70
+ suppressed = sum(1 for f in findings if f.suppressed)
71
+
72
+ by_severity = {}
73
+ for f in findings:
74
+ sev = f.severity.value
75
+ by_severity[sev] = by_severity.get(sev, 0) + 1
76
+
77
+ by_category = {}
78
+ for f in findings:
79
+ cat = f.category.value
80
+ by_category[cat] = by_category.get(cat, 0) + 1
81
+
82
+ return {
83
+ "total": total,
84
+ "actionable": actionable,
85
+ "suppressed": suppressed,
86
+ "by_severity": by_severity,
87
+ "by_category": by_category,
88
+ "risk_score": self._calculate_risk_score(findings),
89
+ }
90
+
91
+ def _finding_to_dict(self, finding: Finding) -> Dict[str, Any]:
92
+ """Convert finding to dictionary."""
93
+ return finding.to_dict()
94
+
95
+ def _calculate_risk_score(self, findings: List[Finding]) -> float:
96
+ """Calculate risk score."""
97
+ from agent_audit.models.risk import Severity
98
+
99
+ if not findings:
100
+ return 0.0
101
+
102
+ weights = {
103
+ Severity.CRITICAL: 3.0,
104
+ Severity.HIGH: 2.0,
105
+ Severity.MEDIUM: 1.0,
106
+ Severity.LOW: 0.3,
107
+ Severity.INFO: 0.1,
108
+ }
109
+
110
+ score = sum(
111
+ weights[f.severity] * f.confidence
112
+ for f in findings
113
+ if not f.suppressed
114
+ )
115
+
116
+ return min(10.0, round(score, 2))
117
+
118
+
119
+ def format_json(
120
+ findings: List[Finding],
121
+ scan_path: str = "",
122
+ scanned_files: int = 0,
123
+ pretty: bool = True
124
+ ) -> str:
125
+ """Convenience function to format findings as JSON."""
126
+ formatter = JSONFormatter(pretty=pretty)
127
+ return formatter.format_to_string(findings, scan_path, scanned_files)
128
+
129
+
130
+ def save_json(
131
+ findings: List[Finding],
132
+ output_path: Path,
133
+ scan_path: str = "",
134
+ scanned_files: int = 0
135
+ ):
136
+ """Convenience function to save findings as JSON."""
137
+ formatter = JSONFormatter()
138
+ formatter.save(findings, output_path, scan_path, scanned_files)
@@ -0,0 +1,155 @@
1
+ """SARIF 2.1.0 output formatter for GitHub Code Scanning."""
2
+
3
+ import json
4
+ from typing import List, Dict, Any
5
+ from pathlib import Path
6
+
7
+ from agent_audit.models.finding import Finding
8
+ from agent_audit.models.risk import Severity
9
+ from agent_audit.version import __version__
10
+
11
+
12
+ class SARIFFormatter:
13
+ """
14
+ SARIF 2.1.0 formatter for GitHub Code Scanning.
15
+
16
+ Produces SARIF-compliant JSON output that can be uploaded to
17
+ GitHub's code scanning feature.
18
+ """
19
+
20
+ SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
21
+ SARIF_VERSION = "2.1.0"
22
+
23
+ def __init__(self, tool_name: str = "agent-audit"):
24
+ self.tool_name = tool_name
25
+
26
+ def format(self, findings: List[Finding]) -> Dict[str, Any]:
27
+ """
28
+ Format findings as SARIF.
29
+
30
+ Args:
31
+ findings: List of findings to format
32
+
33
+ Returns:
34
+ SARIF document as dictionary
35
+ """
36
+ rules = self._extract_rules(findings)
37
+ results = [self._finding_to_result(f) for f in findings]
38
+
39
+ return {
40
+ "$schema": self.SARIF_SCHEMA,
41
+ "version": self.SARIF_VERSION,
42
+ "runs": [{
43
+ "tool": {
44
+ "driver": {
45
+ "name": self.tool_name,
46
+ "version": __version__,
47
+ "informationUri": "https://github.com/your-org/agent-audit",
48
+ "rules": rules
49
+ }
50
+ },
51
+ "results": results
52
+ }]
53
+ }
54
+
55
+ def format_to_string(self, findings: List[Finding], indent: int = 2) -> str:
56
+ """Format findings as SARIF JSON string."""
57
+ sarif = self.format(findings)
58
+ return json.dumps(sarif, indent=indent)
59
+
60
+ def save(self, findings: List[Finding], output_path: Path):
61
+ """Save findings as SARIF file."""
62
+ sarif_str = self.format_to_string(findings)
63
+ output_path.write_text(sarif_str, encoding="utf-8")
64
+
65
+ def _extract_rules(self, findings: List[Finding]) -> List[Dict[str, Any]]:
66
+ """Extract unique rules from findings."""
67
+ rules_map: Dict[str, Dict[str, Any]] = {}
68
+
69
+ for finding in findings:
70
+ if finding.rule_id not in rules_map:
71
+ rule = {
72
+ "id": finding.rule_id,
73
+ "name": finding.title,
74
+ "shortDescription": {
75
+ "text": finding.title
76
+ },
77
+ "fullDescription": {
78
+ "text": finding.description
79
+ },
80
+ "defaultConfiguration": {
81
+ "level": self._severity_to_level(finding.severity)
82
+ },
83
+ "properties": {
84
+ "security-severity": self._severity_to_score(finding.severity)
85
+ }
86
+ }
87
+
88
+ # Add help text if remediation available
89
+ if finding.remediation:
90
+ rule["help"] = {
91
+ "text": finding.remediation.description,
92
+ "markdown": finding.remediation.description
93
+ }
94
+
95
+ # Add CWE/OWASP tags
96
+ tags = []
97
+ if finding.cwe_id:
98
+ tags.append(f"external/cwe/{finding.cwe_id.lower()}")
99
+ if finding.owasp_id:
100
+ tags.append(f"external/owasp/{finding.owasp_id}")
101
+ if finding.category:
102
+ tags.append(finding.category.value)
103
+ if tags:
104
+ rule["properties"]["tags"] = tags
105
+
106
+ rules_map[finding.rule_id] = rule
107
+
108
+ return list(rules_map.values())
109
+
110
+ def _finding_to_result(self, finding: Finding) -> Dict[str, Any]:
111
+ """Convert a Finding to a SARIF result."""
112
+ result = finding.to_sarif()
113
+
114
+ # Add suppression info if suppressed
115
+ if finding.suppressed:
116
+ result["suppressions"] = [{
117
+ "kind": "inSource" if "noaudit" in (finding.suppressed_reason or "") else "external",
118
+ "justification": finding.suppressed_reason or "Suppressed by configuration"
119
+ }]
120
+
121
+ return result
122
+
123
+ def _severity_to_level(self, severity: Severity) -> str:
124
+ """Map severity to SARIF level."""
125
+ mapping = {
126
+ Severity.CRITICAL: "error",
127
+ Severity.HIGH: "error",
128
+ Severity.MEDIUM: "warning",
129
+ Severity.LOW: "note",
130
+ Severity.INFO: "none",
131
+ }
132
+ return mapping[severity]
133
+
134
+ def _severity_to_score(self, severity: Severity) -> str:
135
+ """Map severity to security-severity score (1.0-10.0)."""
136
+ mapping = {
137
+ Severity.CRITICAL: "9.0",
138
+ Severity.HIGH: "7.0",
139
+ Severity.MEDIUM: "5.0",
140
+ Severity.LOW: "3.0",
141
+ Severity.INFO: "1.0",
142
+ }
143
+ return mapping[severity]
144
+
145
+
146
+ def format_sarif(findings: List[Finding]) -> str:
147
+ """Convenience function to format findings as SARIF."""
148
+ formatter = SARIFFormatter()
149
+ return formatter.format_to_string(findings)
150
+
151
+
152
+ def save_sarif(findings: List[Finding], output_path: Path):
153
+ """Convenience function to save findings as SARIF."""
154
+ formatter = SARIFFormatter()
155
+ formatter.save(findings, output_path)
@@ -0,0 +1,221 @@
1
+ """Terminal formatter with Rich output."""
2
+
3
+ from typing import List, Dict
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.text import Text
7
+
8
+ from agent_audit.models.finding import Finding
9
+ from agent_audit.models.risk import Severity
10
+
11
+ console = Console()
12
+
13
+
14
+ class TerminalFormatter:
15
+ """Rich terminal output formatter for scan results."""
16
+
17
+ SEVERITY_COLORS = {
18
+ Severity.CRITICAL: "red bold",
19
+ Severity.HIGH: "red",
20
+ Severity.MEDIUM: "yellow",
21
+ Severity.LOW: "blue",
22
+ Severity.INFO: "dim",
23
+ }
24
+
25
+ SEVERITY_ICONS = {
26
+ Severity.CRITICAL: "🔴",
27
+ Severity.HIGH: "🟠",
28
+ Severity.MEDIUM: "🟡",
29
+ Severity.LOW: "🔵",
30
+ Severity.INFO: "⚪",
31
+ }
32
+
33
+ def __init__(self, verbose: bool = False, quiet: bool = False):
34
+ self.verbose = verbose
35
+ self.quiet = quiet
36
+
37
+ def format_findings(
38
+ self,
39
+ findings: List[Finding],
40
+ scan_path: str,
41
+ scanned_files: int = 0
42
+ ):
43
+ """Format and display findings."""
44
+ if self.quiet and not findings:
45
+ return
46
+
47
+ # Header
48
+ self._print_header(scan_path, scanned_files, findings)
49
+
50
+ if not findings:
51
+ console.print("[green]✓ No security issues found![/green]")
52
+ return
53
+
54
+ # Group findings by severity
55
+ by_severity = self._group_by_severity(findings)
56
+
57
+ # Print findings by severity (highest first)
58
+ for severity in [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM,
59
+ Severity.LOW, Severity.INFO]:
60
+ severity_findings = by_severity.get(severity, [])
61
+ if severity_findings:
62
+ self._print_severity_section(severity, severity_findings)
63
+
64
+ # Summary
65
+ self._print_summary(findings)
66
+
67
+ def _print_header(
68
+ self,
69
+ scan_path: str,
70
+ scanned_files: int,
71
+ findings: List[Finding]
72
+ ):
73
+ """Print the report header."""
74
+ risk_score = self._calculate_risk_score(findings)
75
+ risk_color = "green" if risk_score < 4 else "yellow" if risk_score < 7 else "red"
76
+
77
+ header = Text()
78
+ header.append("Agent Audit Security Report\n", style="bold")
79
+ header.append(f"Scanned: {scan_path}\n", style="dim")
80
+ if scanned_files:
81
+ header.append(f"Files analyzed: {scanned_files}\n", style="dim")
82
+ header.append("Risk Score: ", style="dim")
83
+ header.append(f"{risk_score:.1f}/10", style=f"bold {risk_color}")
84
+
85
+ console.print(Panel(header, border_style=risk_color))
86
+ console.print()
87
+
88
+ def _print_severity_section(
89
+ self,
90
+ severity: Severity,
91
+ findings: List[Finding]
92
+ ):
93
+ """Print findings for a severity level."""
94
+ icon = self.SEVERITY_ICONS[severity]
95
+ color = self.SEVERITY_COLORS[severity]
96
+ count = len(findings)
97
+
98
+ console.print(f"{icon} [{color}]{severity.value.upper()} ({count})[/{color}]")
99
+ console.print()
100
+
101
+ for finding in findings:
102
+ self._print_finding(finding)
103
+
104
+ console.print()
105
+
106
+ def _print_finding(self, finding: Finding):
107
+ """Print a single finding."""
108
+ color = self.SEVERITY_COLORS[finding.severity]
109
+
110
+ # Title and confidence
111
+ confidence_str = ""
112
+ if finding.confidence < 1.0:
113
+ confidence_str = f" (confidence: {finding.confidence:.0%})"
114
+
115
+ console.print(f" [{color}]{finding.rule_id}[/{color}]: {finding.title}{confidence_str}")
116
+
117
+ # Location
118
+ loc = finding.location
119
+ console.print(f" [dim]Location:[/dim] {loc.file_path}:{loc.start_line}")
120
+
121
+ # Code snippet
122
+ if loc.snippet:
123
+ console.print(f" [dim]Code:[/dim] {loc.snippet[:80]}...")
124
+
125
+ # Remediation
126
+ if finding.remediation and self.verbose:
127
+ console.print(f" [dim]Fix:[/dim] {finding.remediation.description[:100]}...")
128
+
129
+ console.print()
130
+
131
+ def _print_summary(self, findings: List[Finding]):
132
+ """Print summary table."""
133
+ # Count by severity
134
+ counts = {s: 0 for s in Severity}
135
+ for f in findings:
136
+ counts[f.severity] += 1
137
+
138
+ suppressed = sum(1 for f in findings if f.suppressed)
139
+
140
+ # Summary line
141
+ parts = []
142
+ for severity in [Severity.CRITICAL, Severity.HIGH, Severity.MEDIUM,
143
+ Severity.LOW, Severity.INFO]:
144
+ if counts[severity] > 0:
145
+ color = self.SEVERITY_COLORS[severity]
146
+ parts.append(f"[{color}]{counts[severity]} {severity.value}[/{color}]")
147
+
148
+ console.print("━" * 50)
149
+ console.print(f"[bold]Findings:[/bold] {', '.join(parts)}")
150
+
151
+ if suppressed:
152
+ console.print(f"[dim]Suppressed: {suppressed} (configure in .agent-audit.yaml)[/dim]")
153
+
154
+ # Risk bar
155
+ risk_score = self._calculate_risk_score(findings)
156
+ self._print_risk_bar(risk_score)
157
+
158
+ def _print_risk_bar(self, risk_score: float):
159
+ """Print a visual risk score bar."""
160
+ bar_width = 30
161
+ filled = int((risk_score / 10) * bar_width)
162
+
163
+ if risk_score < 4:
164
+ color = "green"
165
+ label = "LOW"
166
+ elif risk_score < 7:
167
+ color = "yellow"
168
+ label = "MEDIUM"
169
+ else:
170
+ color = "red"
171
+ label = "HIGH"
172
+
173
+ bar = "█" * filled + "░" * (bar_width - filled)
174
+ console.print(f"[bold]Risk Score:[/bold] [{color}]{bar}[/{color}] {risk_score:.1f}/10 ({label})")
175
+
176
+ def _calculate_risk_score(self, findings: List[Finding]) -> float:
177
+ """Calculate overall risk score from findings."""
178
+ if not findings:
179
+ return 0.0
180
+
181
+ severity_weights = {
182
+ Severity.CRITICAL: 3.0,
183
+ Severity.HIGH: 2.0,
184
+ Severity.MEDIUM: 1.0,
185
+ Severity.LOW: 0.3,
186
+ Severity.INFO: 0.1,
187
+ }
188
+
189
+ score = sum(
190
+ severity_weights[f.severity] * f.confidence
191
+ for f in findings
192
+ if not f.suppressed
193
+ )
194
+
195
+ return min(10.0, score)
196
+
197
+ def _group_by_severity(
198
+ self,
199
+ findings: List[Finding]
200
+ ) -> Dict[Severity, List[Finding]]:
201
+ """Group findings by severity."""
202
+ groups: Dict[Severity, List[Finding]] = {}
203
+ for finding in findings:
204
+ if finding.suppressed and not self.verbose:
205
+ continue
206
+ if finding.severity not in groups:
207
+ groups[finding.severity] = []
208
+ groups[finding.severity].append(finding)
209
+ return groups
210
+
211
+
212
+ def format_scan_results(
213
+ findings: List[Finding],
214
+ scan_path: str,
215
+ scanned_files: int = 0,
216
+ verbose: bool = False,
217
+ quiet: bool = False
218
+ ):
219
+ """Convenience function to format scan results."""
220
+ formatter = TerminalFormatter(verbose=verbose, quiet=quiet)
221
+ formatter.format_findings(findings, scan_path, scanned_files)
@@ -0,0 +1,34 @@
1
+ """CLI main entry point for agent-audit."""
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from agent_audit.version import __version__
7
+
8
+ console = Console()
9
+
10
+
11
+ @click.group()
12
+ @click.version_option(version=__version__)
13
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
14
+ @click.option('--quiet', '-q', is_flag=True, help='Only show errors')
15
+ @click.pass_context
16
+ def cli(ctx: click.Context, verbose: bool, quiet: bool):
17
+ """Agent Audit - Security scanner for AI agents and MCP configurations."""
18
+ ctx.ensure_object(dict)
19
+ ctx.obj['verbose'] = verbose
20
+ ctx.obj['quiet'] = quiet
21
+
22
+
23
+ # Register commands - imports here to avoid circular imports
24
+ from agent_audit.cli.commands.inspect import inspect # noqa: E402
25
+ from agent_audit.cli.commands.scan import scan # noqa: E402
26
+ from agent_audit.cli.commands.init import init # noqa: E402
27
+
28
+ cli.add_command(inspect)
29
+ cli.add_command(scan)
30
+ cli.add_command(init)
31
+
32
+
33
+ if __name__ == '__main__':
34
+ cli()
@@ -0,0 +1 @@
1
+ """Configuration management for agent-audit."""