|
29 | 29 | """ |
30 | 30 |
|
31 | 31 | import logging |
| 32 | +import os |
32 | 33 | from typing import Any |
33 | 34 |
|
34 | 35 | from hybrid.models import HybridFinding |
@@ -241,14 +242,41 @@ def run_api_security(scanner: Any, target_path: str, logger: logging.Logger) -> |
241 | 242 | return findings |
242 | 243 |
|
243 | 244 |
|
| 245 | +def _discover_openapi_spec(target_path: str, logger: logging.Logger) -> str | None: |
| 246 | + """Auto-discover OpenAPI/Swagger spec files in the target directory.""" |
| 247 | + import glob as glob_mod |
| 248 | + spec_patterns = [ |
| 249 | + "openapi.json", "openapi.yaml", "openapi.yml", |
| 250 | + "swagger.json", "swagger.yaml", "swagger.yml", |
| 251 | + "**/openapi.json", "**/openapi.yaml", "**/openapi.yml", |
| 252 | + "**/swagger.json", "**/swagger.yaml", "**/swagger.yml", |
| 253 | + "api-spec.*", "api-docs.*", |
| 254 | + ] |
| 255 | + for pattern in spec_patterns: |
| 256 | + matches = glob_mod.glob(os.path.join(target_path, pattern), recursive=True) |
| 257 | + if matches: |
| 258 | + logger.info(f" 🔍 DAST: Auto-discovered OpenAPI spec: {matches[0]}") |
| 259 | + return matches[0] |
| 260 | + return None |
| 261 | + |
| 262 | + |
244 | 263 | def run_dast(scanner: Any, target_path: str, logger: logging.Logger, config: dict, dast_target_url: str | None = None) -> list[HybridFinding]: |
245 | | - """Run DAST Scanner and convert to HybridFinding format""" |
| 264 | + """Run DAST Scanner and convert to HybridFinding format. |
| 265 | +
|
| 266 | + If no --dast-target-url is provided, auto-discovers OpenAPI/Swagger |
| 267 | + spec files in the target directory for static DAST analysis. |
| 268 | + """ |
246 | 269 | findings = [] |
247 | 270 |
|
248 | | - # DAST requires a target URL |
| 271 | + # If no URL provided, try to auto-discover an OpenAPI spec |
249 | 272 | if not dast_target_url: |
250 | | - logger.info(" ℹ️ DAST: No target URL provided, skipping") |
251 | | - return findings |
| 273 | + openapi_spec = _discover_openapi_spec(target_path, logger) |
| 274 | + if openapi_spec: |
| 275 | + # Reconfigure scanner with discovered spec |
| 276 | + scanner.openapi_spec = openapi_spec |
| 277 | + else: |
| 278 | + logger.info(" ℹ️ DAST: No target URL or OpenAPI spec found, skipping") |
| 279 | + return findings |
252 | 280 |
|
253 | 281 | try: |
254 | 282 | # Run DAST scanner |
@@ -325,32 +353,52 @@ def run_supply_chain(scanner: Any, target_path: str, logger: logging.Logger) -> |
325 | 353 |
|
326 | 354 |
|
327 | 355 | def run_fuzzing(scanner: Any, target_path: str, logger: logging.Logger) -> list[HybridFinding]: |
328 | | - """Run Intelligent Fuzzing Engine and convert to HybridFinding format""" |
| 356 | + """Run Intelligent Fuzzing Engine and convert to HybridFinding format. |
| 357 | +
|
| 358 | + Discovers Python files with parseable functions and fuzzes them |
| 359 | + using FuzzingEngine.fuzz_function(). |
| 360 | + """ |
329 | 361 | findings = [] |
330 | 362 |
|
331 | 363 | try: |
332 | | - # Run Fuzzing scanner |
333 | | - fuzzing_result = scanner.scan(target_path) |
334 | | - |
335 | | - # Convert to HybridFinding format |
336 | | - if isinstance(fuzzing_result, list): |
337 | | - for fuzz_finding in fuzzing_result: |
338 | | - finding = HybridFinding( |
339 | | - finding_id=fuzz_finding.get("id", "unknown"), |
340 | | - source_tool="fuzzing", |
341 | | - severity=normalize_severity(fuzz_finding.get("severity", "medium")), |
342 | | - category="security", |
343 | | - title=fuzz_finding.get("title", "Fuzzing Crash"), |
344 | | - description=fuzz_finding.get("description", ""), |
345 | | - file_path=fuzz_finding.get("file_path", target_path), |
346 | | - line_number=fuzz_finding.get("line_number"), |
347 | | - cwe_id=fuzz_finding.get("cwe_id"), |
348 | | - recommendation=fuzz_finding.get("recommendation", ""), |
349 | | - references=fuzz_finding.get("references", []), |
350 | | - confidence=fuzz_finding.get("confidence", 1.0), |
351 | | - llm_enriched=False, |
| 364 | + import glob as glob_mod |
| 365 | + |
| 366 | + # Discover Python files to fuzz (focus on security-sensitive patterns) |
| 367 | + py_files = glob_mod.glob(os.path.join(target_path, "**", "*.py"), recursive=True) |
| 368 | + if not py_files: |
| 369 | + logger.info(" ℹ️ No Python files found for fuzzing") |
| 370 | + return findings |
| 371 | + |
| 372 | + # Fuzz up to 5 files to keep duration reasonable |
| 373 | + fuzz_targets = py_files[:5] |
| 374 | + for py_file in fuzz_targets: |
| 375 | + rel_path = os.path.relpath(py_file, target_path) |
| 376 | + try: |
| 377 | + fuzzing_result = scanner.fuzz_function( |
| 378 | + function_path=py_file, |
| 379 | + function_name="__main__", |
| 380 | + duration_minutes=1, |
352 | 381 | ) |
353 | | - findings.append(finding) |
| 382 | + # Convert crashes from FuzzResult to HybridFinding |
| 383 | + if hasattr(fuzzing_result, "crashes"): |
| 384 | + for crash in fuzzing_result.crashes: |
| 385 | + finding = HybridFinding( |
| 386 | + finding_id=f"fuzz-{crash.crash_id}", |
| 387 | + source_tool="fuzzing", |
| 388 | + severity=normalize_severity(getattr(crash, "severity", "medium")), |
| 389 | + category="security", |
| 390 | + title=f"Fuzzing crash in {rel_path}: {crash.crash_type}", |
| 391 | + description=f"Crash type: {crash.crash_type}\nStack trace: {crash.stack_trace[:500]}", |
| 392 | + file_path=rel_path, |
| 393 | + cwe_id=getattr(crash, "cwe", None), |
| 394 | + recommendation="Review and fix the crash-inducing input handling", |
| 395 | + confidence=1.0 if crash.reproducible else 0.7, |
| 396 | + llm_enriched=False, |
| 397 | + ) |
| 398 | + findings.append(finding) |
| 399 | + except Exception as e: |
| 400 | + logger.debug(f" Fuzzing {rel_path} skipped: {e}") |
| 401 | + continue |
354 | 402 |
|
355 | 403 | except Exception as e: |
356 | 404 | logger.error(f"❌ Fuzzing failed: {e}") |
@@ -455,34 +503,33 @@ def run_remediation(engine: Any, findings: list[HybridFinding], logger: logging. |
455 | 503 |
|
456 | 504 |
|
457 | 505 | def run_runtime_security(monitor: Any, target_path: str, logger: logging.Logger, monitoring_duration: int) -> list[HybridFinding]: |
458 | | - """Run Container Runtime Security Monitoring""" |
| 506 | + """Run Container Runtime Security Monitoring using Falco-based monitor_realtime().""" |
459 | 507 | findings = [] |
460 | 508 |
|
461 | 509 | try: |
462 | 510 | logger.info(f" 🐳 Monitoring runtime security for {monitoring_duration}s...") |
463 | 511 |
|
464 | | - # Run runtime security monitor |
465 | | - runtime_result = monitor.monitor(target_path) |
| 512 | + # Use monitor_realtime() which returns List[ThreatAlert] |
| 513 | + alerts = monitor.monitor_realtime( |
| 514 | + duration_seconds=monitoring_duration, |
| 515 | + ) |
466 | 516 |
|
467 | | - # Convert to HybridFinding format |
468 | | - if isinstance(runtime_result, list): |
469 | | - for runtime_finding in runtime_result: |
470 | | - finding = HybridFinding( |
471 | | - finding_id=runtime_finding.get("id", "unknown"), |
472 | | - source_tool="runtime-security", |
473 | | - severity=normalize_severity(runtime_finding.get("severity", "medium")), |
474 | | - category="runtime", |
475 | | - title=runtime_finding.get("title", "Runtime Security Threat"), |
476 | | - description=runtime_finding.get("description", ""), |
477 | | - file_path=runtime_finding.get("file_path", target_path), |
478 | | - line_number=runtime_finding.get("line_number"), |
479 | | - cwe_id=runtime_finding.get("cwe_id"), |
480 | | - recommendation=runtime_finding.get("recommendation", ""), |
481 | | - references=runtime_finding.get("references", []), |
482 | | - confidence=runtime_finding.get("confidence", 0.9), |
483 | | - llm_enriched=False, |
484 | | - ) |
485 | | - findings.append(finding) |
| 517 | + # Convert ThreatAlert objects to HybridFinding format |
| 518 | + for alert in (alerts or []): |
| 519 | + finding = HybridFinding( |
| 520 | + finding_id=f"runtime-{getattr(alert, 'alert_id', 'unknown')}", |
| 521 | + source_tool="runtime-security", |
| 522 | + severity=normalize_severity(getattr(alert, "severity", "medium")), |
| 523 | + category="runtime", |
| 524 | + title=getattr(alert, "title", "Runtime Security Threat"), |
| 525 | + description=getattr(alert, "description", str(alert)), |
| 526 | + file_path=getattr(alert, "container_id", target_path), |
| 527 | + cwe_id=getattr(alert, "cwe_id", None), |
| 528 | + recommendation=getattr(alert, "recommendation", "Review runtime security event"), |
| 529 | + confidence=0.9, |
| 530 | + llm_enriched=False, |
| 531 | + ) |
| 532 | + findings.append(finding) |
486 | 533 |
|
487 | 534 | except Exception as e: |
488 | 535 | logger.error(f"❌ Runtime security monitoring failed: {e}") |
|
0 commit comments