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,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)