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.
- agent_audit/__init__.py +3 -0
- agent_audit/__main__.py +13 -0
- agent_audit/cli/__init__.py +1 -0
- agent_audit/cli/commands/__init__.py +1 -0
- agent_audit/cli/commands/init.py +44 -0
- agent_audit/cli/commands/inspect.py +236 -0
- agent_audit/cli/commands/scan.py +329 -0
- agent_audit/cli/formatters/__init__.py +1 -0
- agent_audit/cli/formatters/json.py +138 -0
- agent_audit/cli/formatters/sarif.py +155 -0
- agent_audit/cli/formatters/terminal.py +221 -0
- agent_audit/cli/main.py +34 -0
- agent_audit/config/__init__.py +1 -0
- agent_audit/config/ignore.py +477 -0
- agent_audit/core_utils/__init__.py +1 -0
- agent_audit/models/__init__.py +18 -0
- agent_audit/models/finding.py +159 -0
- agent_audit/models/risk.py +77 -0
- agent_audit/models/tool.py +182 -0
- agent_audit/rules/__init__.py +6 -0
- agent_audit/rules/engine.py +503 -0
- agent_audit/rules/loader.py +160 -0
- agent_audit/scanners/__init__.py +5 -0
- agent_audit/scanners/base.py +32 -0
- agent_audit/scanners/config_scanner.py +390 -0
- agent_audit/scanners/mcp_config_scanner.py +321 -0
- agent_audit/scanners/mcp_inspector.py +421 -0
- agent_audit/scanners/python_scanner.py +544 -0
- agent_audit/scanners/secret_scanner.py +521 -0
- agent_audit/utils/__init__.py +21 -0
- agent_audit/utils/compat.py +98 -0
- agent_audit/utils/mcp_client.py +343 -0
- agent_audit/version.py +3 -0
- agent_audit-0.1.0.dist-info/METADATA +219 -0
- agent_audit-0.1.0.dist-info/RECORD +37 -0
- agent_audit-0.1.0.dist-info/WHEEL +4 -0
- 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)
|
agent_audit/cli/main.py
ADDED
|
@@ -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."""
|