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,390 @@
|
|
|
1
|
+
"""Configuration scanner for YAML/JSON config files."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional, Dict, Any, Set
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from agent_audit.scanners.base import BaseScanner, ScanResult
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class ConfigIssue:
|
|
18
|
+
"""A configuration issue."""
|
|
19
|
+
issue_type: str
|
|
20
|
+
description: str
|
|
21
|
+
key_path: str
|
|
22
|
+
current_value: Any
|
|
23
|
+
recommended_value: Optional[Any] = None
|
|
24
|
+
severity: str = "medium"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ConfigScanResult(ScanResult):
|
|
29
|
+
"""Result of configuration scanning."""
|
|
30
|
+
issues: List[ConfigIssue] = field(default_factory=list)
|
|
31
|
+
config_data: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ConfigScanner(BaseScanner):
|
|
35
|
+
"""
|
|
36
|
+
Scanner for YAML/JSON configuration files.
|
|
37
|
+
|
|
38
|
+
Checks for:
|
|
39
|
+
- Dangerous default settings
|
|
40
|
+
- Overly permissive configurations
|
|
41
|
+
- Security misconfigurations
|
|
42
|
+
- Missing security settings
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
name = "Config Scanner"
|
|
46
|
+
|
|
47
|
+
# Configuration patterns to check
|
|
48
|
+
DANGEROUS_PATTERNS = {
|
|
49
|
+
# Debug/development settings that shouldn't be in production
|
|
50
|
+
'debug': {
|
|
51
|
+
'dangerous_values': [True, 'true', 'True', '1', 'yes'],
|
|
52
|
+
'severity': 'high',
|
|
53
|
+
'description': 'Debug mode should be disabled in production'
|
|
54
|
+
},
|
|
55
|
+
'DEBUG': {
|
|
56
|
+
'dangerous_values': [True, 'true', 'True', '1', 'yes'],
|
|
57
|
+
'severity': 'high',
|
|
58
|
+
'description': 'Debug mode should be disabled in production'
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
# SSL/TLS verification
|
|
62
|
+
'verify_ssl': {
|
|
63
|
+
'dangerous_values': [False, 'false', 'False', '0', 'no'],
|
|
64
|
+
'severity': 'critical',
|
|
65
|
+
'description': 'SSL verification should not be disabled'
|
|
66
|
+
},
|
|
67
|
+
'ssl_verify': {
|
|
68
|
+
'dangerous_values': [False, 'false', 'False', '0', 'no'],
|
|
69
|
+
'severity': 'critical',
|
|
70
|
+
'description': 'SSL verification should not be disabled'
|
|
71
|
+
},
|
|
72
|
+
'insecure': {
|
|
73
|
+
'dangerous_values': [True, 'true', 'True', '1', 'yes'],
|
|
74
|
+
'severity': 'critical',
|
|
75
|
+
'description': 'Insecure mode should not be enabled'
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
# Authentication/authorization
|
|
79
|
+
'allow_anonymous': {
|
|
80
|
+
'dangerous_values': [True, 'true', 'True'],
|
|
81
|
+
'severity': 'high',
|
|
82
|
+
'description': 'Anonymous access should be disabled'
|
|
83
|
+
},
|
|
84
|
+
'auth_required': {
|
|
85
|
+
'dangerous_values': [False, 'false', 'False'],
|
|
86
|
+
'severity': 'high',
|
|
87
|
+
'description': 'Authentication should be required'
|
|
88
|
+
},
|
|
89
|
+
'require_authentication': {
|
|
90
|
+
'dangerous_values': [False, 'false', 'False'],
|
|
91
|
+
'severity': 'high',
|
|
92
|
+
'description': 'Authentication should be required'
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
# CORS
|
|
96
|
+
'cors_allow_all': {
|
|
97
|
+
'dangerous_values': [True, 'true', 'True'],
|
|
98
|
+
'severity': 'medium',
|
|
99
|
+
'description': 'CORS should not allow all origins'
|
|
100
|
+
},
|
|
101
|
+
'allowed_origins': {
|
|
102
|
+
'dangerous_values': ['*'],
|
|
103
|
+
'severity': 'medium',
|
|
104
|
+
'description': 'CORS should specify allowed origins'
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
# Admin/root access
|
|
108
|
+
'admin_enabled': {
|
|
109
|
+
'dangerous_values': [True, 'true', 'True'],
|
|
110
|
+
'severity': 'medium',
|
|
111
|
+
'description': 'Admin interface should be carefully controlled'
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
# Sandbox/security modes
|
|
115
|
+
'sandbox': {
|
|
116
|
+
'dangerous_values': [False, 'false', 'False', 'disabled'],
|
|
117
|
+
'severity': 'high',
|
|
118
|
+
'description': 'Sandbox mode should be enabled'
|
|
119
|
+
},
|
|
120
|
+
'safe_mode': {
|
|
121
|
+
'dangerous_values': [False, 'false', 'False', 'disabled'],
|
|
122
|
+
'severity': 'high',
|
|
123
|
+
'description': 'Safe mode should be enabled'
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Required security settings (should be present)
|
|
128
|
+
REQUIRED_SETTINGS = {
|
|
129
|
+
'rate_limit': 'Rate limiting should be configured',
|
|
130
|
+
'max_requests': 'Request limits should be set',
|
|
131
|
+
'timeout': 'Timeouts should be configured',
|
|
132
|
+
'log_level': 'Logging should be configured',
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def __init__(
|
|
136
|
+
self,
|
|
137
|
+
exclude_paths: Optional[List[str]] = None,
|
|
138
|
+
config_filenames: Optional[List[str]] = None
|
|
139
|
+
):
|
|
140
|
+
"""
|
|
141
|
+
Initialize the config scanner.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
exclude_paths: Path patterns to exclude
|
|
145
|
+
config_filenames: Specific config file names to look for
|
|
146
|
+
"""
|
|
147
|
+
self.exclude_paths = set(exclude_paths or [])
|
|
148
|
+
self.config_filenames = set(config_filenames or [
|
|
149
|
+
'config.yaml', 'config.yml', 'config.json',
|
|
150
|
+
'settings.yaml', 'settings.yml', 'settings.json',
|
|
151
|
+
'app.yaml', 'app.yml', 'app.json',
|
|
152
|
+
'.env.yaml', '.env.json',
|
|
153
|
+
'agent.yaml', 'agent.yml', 'agent.json',
|
|
154
|
+
])
|
|
155
|
+
|
|
156
|
+
def scan(self, path: Path) -> List[ConfigScanResult]:
|
|
157
|
+
"""
|
|
158
|
+
Scan for configuration issues.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
path: File or directory to scan
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of scan results
|
|
165
|
+
"""
|
|
166
|
+
results = []
|
|
167
|
+
config_files = self._find_config_files(path)
|
|
168
|
+
|
|
169
|
+
for config_file in config_files:
|
|
170
|
+
result = self._scan_config_file(config_file)
|
|
171
|
+
if result:
|
|
172
|
+
results.append(result)
|
|
173
|
+
|
|
174
|
+
return results
|
|
175
|
+
|
|
176
|
+
def _find_config_files(self, path: Path) -> List[Path]:
|
|
177
|
+
"""Find configuration files to scan."""
|
|
178
|
+
if path.is_file():
|
|
179
|
+
if self._is_config_file(path):
|
|
180
|
+
return [path]
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
config_files = []
|
|
184
|
+
|
|
185
|
+
# Look for known config file names
|
|
186
|
+
for filename in self.config_filenames:
|
|
187
|
+
config_path = path / filename
|
|
188
|
+
if config_path.exists():
|
|
189
|
+
config_files.append(config_path)
|
|
190
|
+
|
|
191
|
+
# Also scan for config files in subdirectories
|
|
192
|
+
for yaml_file in path.rglob('*.yaml'):
|
|
193
|
+
if self._is_config_file(yaml_file):
|
|
194
|
+
config_files.append(yaml_file)
|
|
195
|
+
|
|
196
|
+
for yml_file in path.rglob('*.yml'):
|
|
197
|
+
if self._is_config_file(yml_file):
|
|
198
|
+
config_files.append(yml_file)
|
|
199
|
+
|
|
200
|
+
for json_file in path.rglob('*.json'):
|
|
201
|
+
if self._is_config_file(json_file):
|
|
202
|
+
config_files.append(json_file)
|
|
203
|
+
|
|
204
|
+
# Deduplicate
|
|
205
|
+
return list(set(config_files))
|
|
206
|
+
|
|
207
|
+
def _is_config_file(self, file_path: Path) -> bool:
|
|
208
|
+
"""Check if a file looks like a configuration file."""
|
|
209
|
+
# Check filename
|
|
210
|
+
if file_path.name in self.config_filenames:
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
# Check for config-like names
|
|
214
|
+
name_lower = file_path.name.lower()
|
|
215
|
+
config_indicators = ['config', 'settings', 'agent', 'app']
|
|
216
|
+
if any(ind in name_lower for ind in config_indicators):
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
# Skip excluded paths
|
|
220
|
+
rel_path = str(file_path)
|
|
221
|
+
if any(excl in rel_path for excl in self.exclude_paths):
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
# Skip common non-config directories
|
|
225
|
+
skip_dirs = {'node_modules', 'venv', '.venv', '__pycache__',
|
|
226
|
+
'dist', 'build', '.git', 'test', 'tests'}
|
|
227
|
+
if any(part in skip_dirs for part in file_path.parts):
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
return False
|
|
231
|
+
|
|
232
|
+
def _scan_config_file(self, file_path: Path) -> Optional[ConfigScanResult]:
|
|
233
|
+
"""Scan a single configuration file."""
|
|
234
|
+
try:
|
|
235
|
+
content = file_path.read_text(encoding='utf-8')
|
|
236
|
+
|
|
237
|
+
if file_path.suffix == '.json':
|
|
238
|
+
data = json.loads(content)
|
|
239
|
+
elif file_path.suffix in {'.yaml', '.yml'}:
|
|
240
|
+
data = yaml.safe_load(content)
|
|
241
|
+
else:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
if not isinstance(data, dict):
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
issues = self._analyze_config(data)
|
|
248
|
+
|
|
249
|
+
return ConfigScanResult(
|
|
250
|
+
source_file=str(file_path),
|
|
251
|
+
issues=issues,
|
|
252
|
+
config_data=data
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
except json.JSONDecodeError as e:
|
|
256
|
+
logger.warning(f"JSON parse error in {file_path}: {e}")
|
|
257
|
+
except yaml.YAMLError as e:
|
|
258
|
+
logger.warning(f"YAML parse error in {file_path}: {e}")
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.warning(f"Error scanning {file_path}: {e}")
|
|
261
|
+
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
def _analyze_config(
|
|
265
|
+
self,
|
|
266
|
+
data: Dict[str, Any],
|
|
267
|
+
prefix: str = ""
|
|
268
|
+
) -> List[ConfigIssue]:
|
|
269
|
+
"""Analyze configuration for issues."""
|
|
270
|
+
issues = []
|
|
271
|
+
|
|
272
|
+
# Check for dangerous patterns
|
|
273
|
+
issues.extend(self._check_dangerous_patterns(data, prefix))
|
|
274
|
+
|
|
275
|
+
# Check for missing required settings
|
|
276
|
+
issues.extend(self._check_required_settings(data))
|
|
277
|
+
|
|
278
|
+
# Recursively check nested configs
|
|
279
|
+
for key, value in data.items():
|
|
280
|
+
if isinstance(value, dict):
|
|
281
|
+
nested_prefix = f"{prefix}.{key}" if prefix else key
|
|
282
|
+
issues.extend(self._analyze_config(value, nested_prefix))
|
|
283
|
+
|
|
284
|
+
return issues
|
|
285
|
+
|
|
286
|
+
def _check_dangerous_patterns(
|
|
287
|
+
self,
|
|
288
|
+
data: Dict[str, Any],
|
|
289
|
+
prefix: str = ""
|
|
290
|
+
) -> List[ConfigIssue]:
|
|
291
|
+
"""Check for dangerous configuration patterns."""
|
|
292
|
+
issues = []
|
|
293
|
+
|
|
294
|
+
for key, value in data.items():
|
|
295
|
+
key_lower = key.lower()
|
|
296
|
+
full_path = f"{prefix}.{key}" if prefix else key
|
|
297
|
+
|
|
298
|
+
# Check against known dangerous patterns
|
|
299
|
+
for pattern_key, pattern_info in self.DANGEROUS_PATTERNS.items():
|
|
300
|
+
if pattern_key.lower() == key_lower or pattern_key in key_lower:
|
|
301
|
+
if value in pattern_info['dangerous_values']:
|
|
302
|
+
issue = ConfigIssue(
|
|
303
|
+
issue_type='dangerous_setting',
|
|
304
|
+
description=pattern_info['description'],
|
|
305
|
+
key_path=full_path,
|
|
306
|
+
current_value=value,
|
|
307
|
+
severity=pattern_info['severity']
|
|
308
|
+
)
|
|
309
|
+
issues.append(issue)
|
|
310
|
+
|
|
311
|
+
# Check for permission-related settings
|
|
312
|
+
if 'permission' in key_lower or 'access' in key_lower:
|
|
313
|
+
if value in ['*', 'all', 'any', True]:
|
|
314
|
+
issue = ConfigIssue(
|
|
315
|
+
issue_type='overly_permissive',
|
|
316
|
+
description='Overly permissive access setting detected',
|
|
317
|
+
key_path=full_path,
|
|
318
|
+
current_value=value,
|
|
319
|
+
severity='medium'
|
|
320
|
+
)
|
|
321
|
+
issues.append(issue)
|
|
322
|
+
|
|
323
|
+
# Check for localhost binding (should be configurable)
|
|
324
|
+
if 'host' in key_lower or 'bind' in key_lower:
|
|
325
|
+
if value == '0.0.0.0':
|
|
326
|
+
issue = ConfigIssue(
|
|
327
|
+
issue_type='network_exposure',
|
|
328
|
+
description='Service binds to all interfaces',
|
|
329
|
+
key_path=full_path,
|
|
330
|
+
current_value=value,
|
|
331
|
+
recommended_value='127.0.0.1',
|
|
332
|
+
severity='medium'
|
|
333
|
+
)
|
|
334
|
+
issues.append(issue)
|
|
335
|
+
|
|
336
|
+
return issues
|
|
337
|
+
|
|
338
|
+
def _check_required_settings(
|
|
339
|
+
self,
|
|
340
|
+
data: Dict[str, Any]
|
|
341
|
+
) -> List[ConfigIssue]:
|
|
342
|
+
"""Check for missing required security settings."""
|
|
343
|
+
issues = []
|
|
344
|
+
|
|
345
|
+
# Flatten keys for checking
|
|
346
|
+
all_keys = self._flatten_keys(data)
|
|
347
|
+
all_keys_lower = {k.lower() for k in all_keys}
|
|
348
|
+
|
|
349
|
+
for setting, description in self.REQUIRED_SETTINGS.items():
|
|
350
|
+
setting_lower = setting.lower()
|
|
351
|
+
if not any(setting_lower in k for k in all_keys_lower):
|
|
352
|
+
# Only flag as missing if this looks like a server/service config
|
|
353
|
+
if self._looks_like_service_config(data):
|
|
354
|
+
issue = ConfigIssue(
|
|
355
|
+
issue_type='missing_setting',
|
|
356
|
+
description=description,
|
|
357
|
+
key_path=setting,
|
|
358
|
+
current_value=None,
|
|
359
|
+
severity='low'
|
|
360
|
+
)
|
|
361
|
+
issues.append(issue)
|
|
362
|
+
|
|
363
|
+
return issues
|
|
364
|
+
|
|
365
|
+
def _flatten_keys(
|
|
366
|
+
self,
|
|
367
|
+
data: Dict[str, Any],
|
|
368
|
+
prefix: str = ""
|
|
369
|
+
) -> Set[str]:
|
|
370
|
+
"""Flatten nested dictionary keys."""
|
|
371
|
+
keys = set()
|
|
372
|
+
|
|
373
|
+
for key, value in data.items():
|
|
374
|
+
full_key = f"{prefix}.{key}" if prefix else key
|
|
375
|
+
keys.add(full_key)
|
|
376
|
+
|
|
377
|
+
if isinstance(value, dict):
|
|
378
|
+
keys.update(self._flatten_keys(value, full_key))
|
|
379
|
+
|
|
380
|
+
return keys
|
|
381
|
+
|
|
382
|
+
def _looks_like_service_config(self, data: Dict[str, Any]) -> bool:
|
|
383
|
+
"""Check if config looks like a service configuration."""
|
|
384
|
+
service_indicators = [
|
|
385
|
+
'server', 'host', 'port', 'bind', 'listen',
|
|
386
|
+
'api', 'endpoint', 'service'
|
|
387
|
+
]
|
|
388
|
+
|
|
389
|
+
all_keys_lower = {k.lower() for k in self._flatten_keys(data)}
|
|
390
|
+
return any(ind in ' '.join(all_keys_lower) for ind in service_indicators)
|