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,77 @@
|
|
|
1
|
+
"""Risk assessment models."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _normalize_path(path: str) -> str:
|
|
9
|
+
"""Normalize path to use forward slashes for cross-platform consistency."""
|
|
10
|
+
return path.replace("\\", "/")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Severity(Enum):
|
|
14
|
+
"""Severity levels for findings."""
|
|
15
|
+
CRITICAL = "critical"
|
|
16
|
+
HIGH = "high"
|
|
17
|
+
MEDIUM = "medium"
|
|
18
|
+
LOW = "low"
|
|
19
|
+
INFO = "info"
|
|
20
|
+
|
|
21
|
+
def __lt__(self, other: "Severity") -> bool:
|
|
22
|
+
order = [Severity.INFO, Severity.LOW, Severity.MEDIUM, Severity.HIGH, Severity.CRITICAL]
|
|
23
|
+
return order.index(self) < order.index(other)
|
|
24
|
+
|
|
25
|
+
def __le__(self, other: "Severity") -> bool:
|
|
26
|
+
return self == other or self < other
|
|
27
|
+
|
|
28
|
+
def __gt__(self, other: "Severity") -> bool:
|
|
29
|
+
return not self <= other
|
|
30
|
+
|
|
31
|
+
def __ge__(self, other: "Severity") -> bool:
|
|
32
|
+
return not self < other
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Category(Enum):
|
|
36
|
+
"""Categories for security findings."""
|
|
37
|
+
COMMAND_INJECTION = "command_injection"
|
|
38
|
+
DATA_EXFILTRATION = "data_exfiltration"
|
|
39
|
+
PRIVILEGE_ESCALATION = "privilege_escalation"
|
|
40
|
+
SUPPLY_CHAIN = "supply_chain"
|
|
41
|
+
CREDENTIAL_EXPOSURE = "credential_exposure"
|
|
42
|
+
PROMPT_INJECTION = "prompt_injection"
|
|
43
|
+
EXCESSIVE_PERMISSION = "excessive_permission"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Location:
|
|
48
|
+
"""Code location for a finding."""
|
|
49
|
+
file_path: str
|
|
50
|
+
start_line: int
|
|
51
|
+
end_line: int
|
|
52
|
+
start_column: Optional[int] = None
|
|
53
|
+
end_column: Optional[int] = None
|
|
54
|
+
snippet: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
def __post_init__(self):
|
|
57
|
+
"""Normalize file_path to use forward slashes for cross-platform consistency."""
|
|
58
|
+
self.file_path = _normalize_path(self.file_path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class RiskScore:
|
|
63
|
+
"""Risk score calculation result."""
|
|
64
|
+
score: float # 0.0 - 10.0
|
|
65
|
+
factors: dict # Contributing factors and their weights
|
|
66
|
+
|
|
67
|
+
def is_high_risk(self) -> bool:
|
|
68
|
+
"""Check if this represents high risk."""
|
|
69
|
+
return self.score >= 7.0
|
|
70
|
+
|
|
71
|
+
def is_medium_risk(self) -> bool:
|
|
72
|
+
"""Check if this represents medium risk."""
|
|
73
|
+
return 4.0 <= self.score < 7.0
|
|
74
|
+
|
|
75
|
+
def is_low_risk(self) -> bool:
|
|
76
|
+
"""Check if this represents low risk."""
|
|
77
|
+
return self.score < 4.0
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Tool definition models for agent analysis."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import List, Optional, Set
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PermissionType(Enum):
|
|
9
|
+
"""Permission types that tools may require."""
|
|
10
|
+
FILE_READ = auto()
|
|
11
|
+
FILE_WRITE = auto()
|
|
12
|
+
FILE_DELETE = auto()
|
|
13
|
+
SHELL_EXEC = auto()
|
|
14
|
+
NETWORK_OUTBOUND = auto()
|
|
15
|
+
NETWORK_INBOUND = auto()
|
|
16
|
+
DATABASE_READ = auto()
|
|
17
|
+
DATABASE_WRITE = auto()
|
|
18
|
+
SECRET_ACCESS = auto()
|
|
19
|
+
BROWSER_CONTROL = auto()
|
|
20
|
+
PROCESS_SPAWN = auto()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RiskLevel(Enum):
|
|
24
|
+
"""Risk levels for tools."""
|
|
25
|
+
SAFE = 1
|
|
26
|
+
LOW = 2
|
|
27
|
+
MEDIUM = 3
|
|
28
|
+
HIGH = 4
|
|
29
|
+
CRITICAL = 5
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ToolParameter:
|
|
34
|
+
"""Tool parameter definition."""
|
|
35
|
+
name: str
|
|
36
|
+
type: str
|
|
37
|
+
required: bool = False
|
|
38
|
+
description: Optional[str] = None
|
|
39
|
+
allows_arbitrary_input: bool = False
|
|
40
|
+
sanitization_present: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ToolDefinition:
|
|
45
|
+
"""
|
|
46
|
+
Agent tool definition.
|
|
47
|
+
|
|
48
|
+
Represents a tool that an agent can call, with its permissions,
|
|
49
|
+
risk level, and security properties.
|
|
50
|
+
"""
|
|
51
|
+
name: str
|
|
52
|
+
description: str
|
|
53
|
+
source_file: str
|
|
54
|
+
source_line: int
|
|
55
|
+
|
|
56
|
+
permissions: Set[PermissionType] = field(default_factory=set)
|
|
57
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
58
|
+
parameters: List[ToolParameter] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
# MCP-specific fields
|
|
61
|
+
mcp_server: Optional[str] = None
|
|
62
|
+
mcp_server_verified: bool = False
|
|
63
|
+
|
|
64
|
+
# Security properties
|
|
65
|
+
has_input_validation: bool = False
|
|
66
|
+
has_output_sanitization: bool = False
|
|
67
|
+
runs_in_sandbox: bool = False
|
|
68
|
+
requires_approval: bool = False
|
|
69
|
+
|
|
70
|
+
# Capability flags (derived from permissions)
|
|
71
|
+
can_execute_code: bool = False
|
|
72
|
+
can_access_filesystem: bool = False
|
|
73
|
+
can_access_network: bool = False
|
|
74
|
+
can_access_secrets: bool = False
|
|
75
|
+
|
|
76
|
+
def calculate_risk_score(self) -> float:
|
|
77
|
+
"""
|
|
78
|
+
Calculate risk score (0.0 - 10.0).
|
|
79
|
+
|
|
80
|
+
Takes into account:
|
|
81
|
+
- Permission weights (shell exec is highest risk)
|
|
82
|
+
- Mitigating factors (input validation, sandbox)
|
|
83
|
+
- MCP server verification status
|
|
84
|
+
"""
|
|
85
|
+
score = 0.0
|
|
86
|
+
|
|
87
|
+
# Permission weights
|
|
88
|
+
permission_weights = {
|
|
89
|
+
PermissionType.SHELL_EXEC: 3.0,
|
|
90
|
+
PermissionType.SECRET_ACCESS: 2.5,
|
|
91
|
+
PermissionType.FILE_DELETE: 2.0,
|
|
92
|
+
PermissionType.DATABASE_WRITE: 2.0,
|
|
93
|
+
PermissionType.NETWORK_OUTBOUND: 1.5,
|
|
94
|
+
PermissionType.FILE_WRITE: 1.5,
|
|
95
|
+
PermissionType.PROCESS_SPAWN: 2.0,
|
|
96
|
+
PermissionType.BROWSER_CONTROL: 1.5,
|
|
97
|
+
PermissionType.FILE_READ: 0.5,
|
|
98
|
+
PermissionType.DATABASE_READ: 0.5,
|
|
99
|
+
PermissionType.NETWORK_INBOUND: 1.0,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for perm in self.permissions:
|
|
103
|
+
score += permission_weights.get(perm, 0.5)
|
|
104
|
+
|
|
105
|
+
# Mitigating factors
|
|
106
|
+
if self.has_input_validation:
|
|
107
|
+
score *= 0.7
|
|
108
|
+
if self.runs_in_sandbox:
|
|
109
|
+
score *= 0.5
|
|
110
|
+
if self.requires_approval:
|
|
111
|
+
score *= 0.6
|
|
112
|
+
|
|
113
|
+
# MCP verification penalty
|
|
114
|
+
if self.mcp_server and not self.mcp_server_verified:
|
|
115
|
+
score *= 1.3
|
|
116
|
+
|
|
117
|
+
return min(10.0, score)
|
|
118
|
+
|
|
119
|
+
def infer_risk_level(self) -> RiskLevel:
|
|
120
|
+
"""Infer risk level from calculated score."""
|
|
121
|
+
score = self.calculate_risk_score()
|
|
122
|
+
if score >= 8.0:
|
|
123
|
+
return RiskLevel.CRITICAL
|
|
124
|
+
elif score >= 6.0:
|
|
125
|
+
return RiskLevel.HIGH
|
|
126
|
+
elif score >= 4.0:
|
|
127
|
+
return RiskLevel.MEDIUM
|
|
128
|
+
elif score >= 2.0:
|
|
129
|
+
return RiskLevel.LOW
|
|
130
|
+
else:
|
|
131
|
+
return RiskLevel.SAFE
|
|
132
|
+
|
|
133
|
+
def update_capability_flags(self):
|
|
134
|
+
"""Update capability flags based on permissions."""
|
|
135
|
+
self.can_execute_code = PermissionType.SHELL_EXEC in self.permissions
|
|
136
|
+
self.can_access_filesystem = any(
|
|
137
|
+
p in self.permissions for p in [
|
|
138
|
+
PermissionType.FILE_READ,
|
|
139
|
+
PermissionType.FILE_WRITE,
|
|
140
|
+
PermissionType.FILE_DELETE
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
self.can_access_network = any(
|
|
144
|
+
p in self.permissions for p in [
|
|
145
|
+
PermissionType.NETWORK_OUTBOUND,
|
|
146
|
+
PermissionType.NETWORK_INBOUND
|
|
147
|
+
]
|
|
148
|
+
)
|
|
149
|
+
self.can_access_secrets = PermissionType.SECRET_ACCESS in self.permissions
|
|
150
|
+
|
|
151
|
+
def to_dict(self) -> dict:
|
|
152
|
+
"""Convert to dictionary for JSON serialization."""
|
|
153
|
+
return {
|
|
154
|
+
"name": self.name,
|
|
155
|
+
"description": self.description,
|
|
156
|
+
"source_file": self.source_file,
|
|
157
|
+
"source_line": self.source_line,
|
|
158
|
+
"permissions": [p.name for p in self.permissions],
|
|
159
|
+
"risk_level": self.risk_level.name,
|
|
160
|
+
"risk_score": self.calculate_risk_score(),
|
|
161
|
+
"parameters": [
|
|
162
|
+
{
|
|
163
|
+
"name": p.name,
|
|
164
|
+
"type": p.type,
|
|
165
|
+
"required": p.required,
|
|
166
|
+
"description": p.description,
|
|
167
|
+
"allows_arbitrary_input": p.allows_arbitrary_input,
|
|
168
|
+
"sanitization_present": p.sanitization_present,
|
|
169
|
+
}
|
|
170
|
+
for p in self.parameters
|
|
171
|
+
],
|
|
172
|
+
"mcp_server": self.mcp_server,
|
|
173
|
+
"mcp_server_verified": self.mcp_server_verified,
|
|
174
|
+
"has_input_validation": self.has_input_validation,
|
|
175
|
+
"has_output_sanitization": self.has_output_sanitization,
|
|
176
|
+
"runs_in_sandbox": self.runs_in_sandbox,
|
|
177
|
+
"requires_approval": self.requires_approval,
|
|
178
|
+
"can_execute_code": self.can_execute_code,
|
|
179
|
+
"can_access_filesystem": self.can_access_filesystem,
|
|
180
|
+
"can_access_network": self.can_access_network,
|
|
181
|
+
"can_access_secrets": self.can_access_secrets,
|
|
182
|
+
}
|