1309 lines
48 KiB
Python
1309 lines
48 KiB
Python
"""007 Full Audit -- Comprehensive 6-phase security audit orchestrator.
|
|
|
|
Executes the complete 007 security audit pipeline:
|
|
Phase 1: Surface Mapping -- file inventory, entry points, dependencies
|
|
Phase 2: Threat Modeling Hints -- identify components for STRIDE analysis
|
|
Phase 3: Security Checklist -- run all scanners, compile results
|
|
Phase 4: Red Team Scenarios -- template-based attack scenarios
|
|
Phase 5: Blue Team Recs -- hardening recommendations per finding
|
|
Phase 6: Verdict -- compute score and emit final verdict
|
|
|
|
Generates a comprehensive Markdown report saved to data/reports/ and prints
|
|
a summary to stdout.
|
|
|
|
Usage:
|
|
python full_audit.py --target /path/to/project
|
|
python full_audit.py --target /path/to/project --output markdown
|
|
python full_audit.py --target /path/to/project --phase 3 --verbose
|
|
python full_audit.py --target /path/to/project --output json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Imports from the 007 config hub (same directory)
|
|
# ---------------------------------------------------------------------------
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from config import ( # noqa: E402
|
|
BASE_DIR,
|
|
DATA_DIR,
|
|
REPORTS_DIR,
|
|
SCANNABLE_EXTENSIONS,
|
|
SKIP_DIRECTORIES,
|
|
SCORING_WEIGHTS,
|
|
SCORING_LABELS,
|
|
SEVERITY,
|
|
LIMITS,
|
|
ensure_directories,
|
|
get_verdict,
|
|
get_timestamp,
|
|
log_audit_event,
|
|
setup_logging,
|
|
calculate_weighted_score,
|
|
)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Import scanners
|
|
# ---------------------------------------------------------------------------
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "scanners"))
|
|
|
|
import secrets_scanner # noqa: E402
|
|
import dependency_scanner # noqa: E402
|
|
import injection_scanner # noqa: E402
|
|
import quick_scan # noqa: E402
|
|
import score_calculator # noqa: E402
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Logger
|
|
# ---------------------------------------------------------------------------
|
|
logger = setup_logging("007-full-audit")
|
|
|
|
|
|
# =========================================================================
|
|
# RED TEAM SCENARIO TEMPLATES
|
|
# =========================================================================
|
|
# Mapping from finding type/pattern -> attack scenario template.
|
|
|
|
_RED_TEAM_TEMPLATES: dict[str, dict] = {
|
|
# --- Secrets ---
|
|
"secret": {
|
|
"title": "Credential Theft via Leaked Secret",
|
|
"persona": "External attacker / Insider",
|
|
"scenario": (
|
|
"Attacker discovers leaked credential ({pattern}) in {file} "
|
|
"and uses it to gain unauthorized access to the associated "
|
|
"service or resource. Depending on the credential scope, "
|
|
"the attacker may escalate to full account takeover."
|
|
),
|
|
"impact": "Unauthorized access, data exfiltration, lateral movement",
|
|
"difficulty": "Easy (if credential is in public repo) / Medium (if private)",
|
|
},
|
|
# --- Injection ---
|
|
"code_injection": {
|
|
"title": "Remote Code Execution via Code Injection",
|
|
"persona": "Malicious user / Compromised agent",
|
|
"scenario": (
|
|
"Attacker crafts malicious input targeting {pattern} in {file}. "
|
|
"The injected code executes in the server context, allowing "
|
|
"arbitrary command execution, data access, or system compromise."
|
|
),
|
|
"impact": "Full server compromise, data breach, service disruption",
|
|
"difficulty": "Medium",
|
|
},
|
|
"command_injection": {
|
|
"title": "System Compromise via Command Injection",
|
|
"persona": "Malicious user / API abuser",
|
|
"scenario": (
|
|
"Attacker injects OS commands through {pattern} in {file}. "
|
|
"The shell executes attacker-controlled commands, enabling "
|
|
"file access, reverse shells, or privilege escalation."
|
|
),
|
|
"impact": "Full system compromise, lateral movement",
|
|
"difficulty": "Medium",
|
|
},
|
|
"sql_injection": {
|
|
"title": "Data Breach via SQL Injection",
|
|
"persona": "Malicious user / Bot",
|
|
"scenario": (
|
|
"Attacker crafts SQL payload targeting {pattern} in {file}. "
|
|
"The malformed query bypasses authentication, extracts sensitive "
|
|
"data, modifies records, or drops tables."
|
|
),
|
|
"impact": "Data breach, data loss, authentication bypass",
|
|
"difficulty": "Easy to Medium",
|
|
},
|
|
"prompt_injection": {
|
|
"title": "AI Manipulation via Prompt Injection",
|
|
"persona": "Malicious user / Compromised data source",
|
|
"scenario": (
|
|
"Attacker injects adversarial prompt through {pattern} in {file}. "
|
|
"The LLM follows injected instructions, potentially exfiltrating "
|
|
"data, bypassing safety controls, or performing unauthorized actions."
|
|
),
|
|
"impact": "Data leakage, unauthorized actions, reputation damage",
|
|
"difficulty": "Easy to Medium",
|
|
},
|
|
"xss": {
|
|
"title": "User Account Takeover via XSS",
|
|
"persona": "Malicious user",
|
|
"scenario": (
|
|
"Attacker injects JavaScript through {pattern} in {file}. "
|
|
"The script executes in victim browsers, stealing session tokens, "
|
|
"redirecting users, or performing actions on their behalf."
|
|
),
|
|
"impact": "Session hijacking, credential theft, phishing",
|
|
"difficulty": "Easy",
|
|
},
|
|
"ssrf": {
|
|
"title": "Internal Network Scanning via SSRF",
|
|
"persona": "External attacker",
|
|
"scenario": (
|
|
"Attacker manipulates server-side request through {pattern} in {file}. "
|
|
"The server makes requests to internal services, cloud metadata endpoints, "
|
|
"or other internal resources on the attacker's behalf."
|
|
),
|
|
"impact": "Internal network exposure, cloud credential theft, data access",
|
|
"difficulty": "Medium",
|
|
},
|
|
"path_traversal": {
|
|
"title": "Sensitive File Access via Path Traversal",
|
|
"persona": "Malicious user",
|
|
"scenario": (
|
|
"Attacker uses directory traversal sequences (../) through {pattern} "
|
|
"in {file} to access files outside the intended directory, "
|
|
"including configuration files, credentials, or system files."
|
|
),
|
|
"impact": "Credential exposure, configuration leak, source code theft",
|
|
"difficulty": "Easy",
|
|
},
|
|
# --- Dependencies ---
|
|
"dependency": {
|
|
"title": "Supply Chain Attack via Vulnerable Dependency",
|
|
"persona": "Supply chain attacker",
|
|
"scenario": (
|
|
"Attacker compromises a dependency ({pattern}) used in {file}. "
|
|
"Malicious code in the dependency executes during install or runtime, "
|
|
"exfiltrating secrets, installing backdoors, or modifying behavior."
|
|
),
|
|
"impact": "Full compromise, backdoor installation, data exfiltration",
|
|
"difficulty": "Hard (requires compromising upstream package)",
|
|
},
|
|
# --- Auth missing ---
|
|
"no_auth": {
|
|
"title": "Unauthorized Access to Unprotected Endpoints",
|
|
"persona": "Any external attacker / Bot",
|
|
"scenario": (
|
|
"Attacker discovers unprotected API endpoints or routes "
|
|
"with no authentication middleware. Direct access allows "
|
|
"data extraction, modification, or service abuse without credentials."
|
|
),
|
|
"impact": "Data breach, unauthorized actions, resource abuse",
|
|
"difficulty": "Easy",
|
|
},
|
|
# --- Dangerous code ---
|
|
"dangerous_code": {
|
|
"title": "Exploitation of Dangerous Code Pattern",
|
|
"persona": "Malicious user / Insider",
|
|
"scenario": (
|
|
"Attacker exploits dangerous code construct ({pattern}) in {file}. "
|
|
"The construct allows unintended behavior such as arbitrary code "
|
|
"execution, deserialization attacks, or unsafe data processing."
|
|
),
|
|
"impact": "Code execution, data manipulation, service disruption",
|
|
"difficulty": "Medium",
|
|
},
|
|
}
|
|
|
|
# Fallback template for finding types not explicitly mapped
|
|
_RED_TEAM_FALLBACK = {
|
|
"title": "Exploitation of Security Weakness",
|
|
"persona": "Opportunistic attacker",
|
|
"scenario": (
|
|
"Attacker discovers and exploits security weakness ({pattern}) "
|
|
"in {file}. The specific impact depends on the context and "
|
|
"the attacker's capabilities."
|
|
),
|
|
"impact": "Variable -- depends on finding severity and context",
|
|
"difficulty": "Variable",
|
|
}
|
|
|
|
|
|
# =========================================================================
|
|
# BLUE TEAM RECOMMENDATION TEMPLATES
|
|
# =========================================================================
|
|
|
|
_BLUE_TEAM_TEMPLATES: dict[str, dict] = {
|
|
"secret": {
|
|
"recommendation": (
|
|
"Move secrets to environment variables, a secrets manager (e.g. AWS "
|
|
"Secrets Manager, HashiCorp Vault), or a .env file excluded from "
|
|
"version control. Add a pre-commit hook (e.g. detect-secrets, "
|
|
"gitleaks) to prevent future leaks. Rotate the compromised credential "
|
|
"immediately."
|
|
),
|
|
"priority": "CRITICAL",
|
|
"effort": "Low",
|
|
},
|
|
"code_injection": {
|
|
"recommendation": (
|
|
"Remove all uses of eval(), exec(), and Function(). If dynamic "
|
|
"code execution is absolutely necessary, use a sandboxed environment "
|
|
"(e.g. RestrictedPython, vm2 in strict mode) with allowlisted "
|
|
"operations only. Never pass user input to code execution functions."
|
|
),
|
|
"priority": "CRITICAL",
|
|
"effort": "Medium",
|
|
},
|
|
"command_injection": {
|
|
"recommendation": (
|
|
"Replace os.system(), os.popen(), and subprocess with shell=True "
|
|
"with subprocess.run() using shell=False and a list of arguments. "
|
|
"Never concatenate user input into shell commands. Validate and "
|
|
"sanitize all inputs. Use shlex.quote() if shell is unavoidable."
|
|
),
|
|
"priority": "CRITICAL",
|
|
"effort": "Low to Medium",
|
|
},
|
|
"sql_injection": {
|
|
"recommendation": (
|
|
"Use parameterized queries (placeholders) for ALL database operations. "
|
|
"Never use f-strings, .format(), or string concatenation in SQL. "
|
|
"Use an ORM (SQLAlchemy, Django ORM) when possible. Add input "
|
|
"validation and type checking before database operations."
|
|
),
|
|
"priority": "CRITICAL",
|
|
"effort": "Low",
|
|
},
|
|
"prompt_injection": {
|
|
"recommendation": (
|
|
"Separate system prompts from user content using proper message "
|
|
"structure (system/user/assistant roles). Never concatenate user "
|
|
"input directly into system prompts. Add input sanitization, "
|
|
"output filtering, and content safety guardrails. Limit tool "
|
|
"access and implement output validation."
|
|
),
|
|
"priority": "HIGH",
|
|
"effort": "Medium",
|
|
},
|
|
"xss": {
|
|
"recommendation": (
|
|
"Never set innerHTML or use dangerouslySetInnerHTML with user content. "
|
|
"Use textContent for safe text insertion. Implement Content Security "
|
|
"Policy (CSP) headers. Use template engines with auto-escaping "
|
|
"(Jinja2 with autoescape, React JSX). Sanitize user HTML with "
|
|
"DOMPurify or bleach."
|
|
),
|
|
"priority": "HIGH",
|
|
"effort": "Low to Medium",
|
|
},
|
|
"ssrf": {
|
|
"recommendation": (
|
|
"Implement URL allowlisting for outbound requests. Block requests "
|
|
"to private IP ranges (10.x, 172.16-31.x, 192.168.x), localhost, "
|
|
"and cloud metadata endpoints (169.254.169.254). Validate and "
|
|
"parse URLs before making requests. Use a dedicated HTTP client "
|
|
"with SSRF protections."
|
|
),
|
|
"priority": "HIGH",
|
|
"effort": "Medium",
|
|
},
|
|
"path_traversal": {
|
|
"recommendation": (
|
|
"Use Path.resolve() and verify the resolved path starts with the "
|
|
"expected base directory. Never pass raw user input to open() or "
|
|
"file operations. Use os.path.realpath() followed by a prefix check. "
|
|
"Implement a file access allowlist."
|
|
),
|
|
"priority": "HIGH",
|
|
"effort": "Low",
|
|
},
|
|
"dependency": {
|
|
"recommendation": (
|
|
"Pin all dependency versions with exact versions (not ranges). "
|
|
"Use lock files (pip freeze, package-lock.json, poetry.lock). "
|
|
"Run regular vulnerability scans (safety, npm audit, Snyk). "
|
|
"Remove unused dependencies. Verify package integrity with hashes."
|
|
),
|
|
"priority": "MEDIUM",
|
|
"effort": "Low",
|
|
},
|
|
"dangerous_code": {
|
|
"recommendation": (
|
|
"Replace dangerous constructs with safe alternatives: "
|
|
"pickle -> json, yaml.load -> yaml.safe_load, eval -> ast.literal_eval "
|
|
"(for literals only). Add input validation before any dynamic operation. "
|
|
"Implement proper error handling and type checking."
|
|
),
|
|
"priority": "HIGH",
|
|
"effort": "Low to Medium",
|
|
},
|
|
"no_auth": {
|
|
"recommendation": (
|
|
"Add authentication middleware to all endpoints except public "
|
|
"health checks. Implement RBAC/ABAC for authorization. Use "
|
|
"established auth libraries (Flask-Login, Passport.js, Django auth). "
|
|
"Add rate limiting to prevent brute force attacks."
|
|
),
|
|
"priority": "CRITICAL",
|
|
"effort": "Medium",
|
|
},
|
|
"permission": {
|
|
"recommendation": (
|
|
"Set restrictive file permissions (600 for secrets, 644 for configs, "
|
|
"755 for executables). Never use 777. Run services as non-root users. "
|
|
"Use chown/chmod to enforce ownership."
|
|
),
|
|
"priority": "MEDIUM",
|
|
"effort": "Low",
|
|
},
|
|
"large_file": {
|
|
"recommendation": (
|
|
"Investigate oversized files for accidentally committed binaries, "
|
|
"databases, or data dumps. Add them to .gitignore. Use Git LFS "
|
|
"for legitimate large files."
|
|
),
|
|
"priority": "LOW",
|
|
"effort": "Low",
|
|
},
|
|
}
|
|
|
|
_BLUE_TEAM_FALLBACK = {
|
|
"recommendation": (
|
|
"Review the finding in context and apply the principle of least "
|
|
"privilege. Add input validation, proper error handling, and "
|
|
"logging. Consult OWASP guidelines for the specific vulnerability type."
|
|
),
|
|
"priority": "MEDIUM",
|
|
"effort": "Variable",
|
|
}
|
|
|
|
|
|
# =========================================================================
|
|
# PHASE IMPLEMENTATIONS
|
|
# =========================================================================
|
|
|
|
def _phase1_surface_mapping(target: Path, verbose: bool = False) -> dict:
|
|
"""Phase 1: Surface Mapping -- inventory files, entry points, dependencies."""
|
|
logger.info("Phase 1: Surface Mapping")
|
|
|
|
files_by_type: dict[str, int] = {}
|
|
entry_points: list[str] = []
|
|
dependency_files: list[str] = []
|
|
config_files: list[str] = []
|
|
total_files = 0
|
|
|
|
_entry_point_patterns = [
|
|
re.compile(r"""(?i)(?:^main\.py|^app\.py|^server\.py|^index\.\w+|^manage\.py)"""),
|
|
re.compile(r"""(?i)(?:^wsgi\.py|^asgi\.py|^gunicorn|^uvicorn)"""),
|
|
re.compile(r"""(?i)(?:^Dockerfile|^docker-compose)"""),
|
|
re.compile(r"""(?i)(?:\.github[/\\]workflows|Jenkinsfile|\.gitlab-ci)"""),
|
|
]
|
|
|
|
_dep_file_names = {
|
|
"requirements.txt", "requirements-dev.txt", "requirements-test.txt",
|
|
"setup.py", "setup.cfg", "pyproject.toml", "Pipfile", "Pipfile.lock",
|
|
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
"go.mod", "go.sum", "Cargo.toml", "Cargo.lock",
|
|
"Gemfile", "Gemfile.lock", "composer.json", "composer.lock",
|
|
}
|
|
|
|
_config_extensions = {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env"}
|
|
|
|
for root, dirs, filenames in os.walk(target):
|
|
dirs[:] = [d for d in dirs if d not in SKIP_DIRECTORIES]
|
|
|
|
for fname in filenames:
|
|
total_files += 1
|
|
fpath = Path(root) / fname
|
|
suffix = fpath.suffix.lower()
|
|
|
|
# Categorize by extension
|
|
ext_key = suffix if suffix else "(no extension)"
|
|
files_by_type[ext_key] = files_by_type.get(ext_key, 0) + 1
|
|
|
|
# Detect entry points
|
|
for pat in _entry_point_patterns:
|
|
if pat.search(fname) or pat.search(str(fpath)):
|
|
entry_points.append(str(fpath))
|
|
break
|
|
|
|
# Detect dependency files
|
|
if fname.lower() in _dep_file_names:
|
|
dependency_files.append(str(fpath))
|
|
|
|
# Detect config files
|
|
if suffix in _config_extensions or fname.lower().startswith(".env"):
|
|
config_files.append(str(fpath))
|
|
|
|
# Sort by count descending
|
|
sorted_types = sorted(files_by_type.items(), key=lambda x: x[1], reverse=True)
|
|
|
|
return {
|
|
"total_files": total_files,
|
|
"files_by_type": dict(sorted_types),
|
|
"entry_points": sorted(set(entry_points)),
|
|
"dependency_files": sorted(set(dependency_files)),
|
|
"config_files": sorted(set(config_files)),
|
|
}
|
|
|
|
|
|
def _phase2_threat_modeling_hints(surface_map: dict, findings: list[dict]) -> dict:
|
|
"""Phase 2: Threat Modeling Hints -- identify components for STRIDE analysis."""
|
|
logger.info("Phase 2: Threat Modeling Hints")
|
|
|
|
components: list[dict] = []
|
|
|
|
# Entry points are high-value STRIDE targets
|
|
for ep in surface_map.get("entry_points", []):
|
|
components.append({
|
|
"component": ep,
|
|
"type": "entry_point",
|
|
"stride_focus": ["Spoofing", "Tampering", "Elevation of Privilege"],
|
|
"reason": "Application entry point -- critical for authentication and authorization",
|
|
})
|
|
|
|
# Dependency files = supply chain
|
|
for dep_file in surface_map.get("dependency_files", []):
|
|
components.append({
|
|
"component": dep_file,
|
|
"type": "dependency_manifest",
|
|
"stride_focus": ["Tampering", "Elevation of Privilege"],
|
|
"reason": "Dependency manifest -- supply chain attack vector",
|
|
})
|
|
|
|
# Config files = information disclosure
|
|
for cfg in surface_map.get("config_files", []):
|
|
components.append({
|
|
"component": cfg,
|
|
"type": "configuration",
|
|
"stride_focus": ["Information Disclosure", "Tampering"],
|
|
"reason": "Configuration file -- may contain secrets or security settings",
|
|
})
|
|
|
|
# Files with critical findings
|
|
critical_files: set[str] = set()
|
|
for f in findings:
|
|
if f.get("severity") in ("CRITICAL", "HIGH"):
|
|
critical_files.add(f.get("file", ""))
|
|
|
|
for cf in sorted(critical_files):
|
|
if cf:
|
|
components.append({
|
|
"component": cf,
|
|
"type": "high_risk_source",
|
|
"stride_focus": [
|
|
"Spoofing", "Tampering", "Repudiation",
|
|
"Information Disclosure", "Denial of Service",
|
|
"Elevation of Privilege",
|
|
],
|
|
"reason": "Source file with CRITICAL/HIGH severity findings",
|
|
})
|
|
|
|
return {
|
|
"components_for_stride": components,
|
|
"total_components": len(components),
|
|
"recommendation": (
|
|
"Run a formal STRIDE analysis on each component above. "
|
|
"For each STRIDE category, document: attack vector, impact (1-5), "
|
|
"probability (1-5), and proposed mitigation."
|
|
),
|
|
}
|
|
|
|
|
|
def _phase3_security_checklist(
|
|
secrets_report: dict,
|
|
dep_report: dict,
|
|
inj_report: dict,
|
|
quick_report: dict,
|
|
) -> dict:
|
|
"""Phase 3: Security Checklist -- compile all scanner results."""
|
|
logger.info("Phase 3: Security Checklist")
|
|
|
|
checklist: list[dict] = []
|
|
|
|
# Secrets check
|
|
secrets_count = secrets_report.get("total_findings", 0)
|
|
checklist.append({
|
|
"check": "No hardcoded secrets in source code",
|
|
"status": "PASS" if secrets_count == 0 else "FAIL",
|
|
"details": f"{secrets_count} secret(s) detected",
|
|
"scanner": "secrets_scanner",
|
|
})
|
|
|
|
# Dependency check
|
|
dep_score = dep_report.get("score", 0)
|
|
dep_count = dep_report.get("total_findings", 0)
|
|
checklist.append({
|
|
"check": "Dependencies are secure and pinned",
|
|
"status": "PASS" if dep_score >= 80 else ("WARN" if dep_score >= 50 else "FAIL"),
|
|
"details": f"{dep_count} finding(s), score={dep_score}",
|
|
"scanner": "dependency_scanner",
|
|
})
|
|
|
|
# Injection check
|
|
inj_count = inj_report.get("total_findings", 0)
|
|
inj_critical = inj_report.get("severity_counts", {}).get("CRITICAL", 0)
|
|
checklist.append({
|
|
"check": "No injection vulnerabilities",
|
|
"status": "PASS" if inj_count == 0 else ("FAIL" if inj_critical > 0 else "WARN"),
|
|
"details": f"{inj_count} finding(s), {inj_critical} CRITICAL",
|
|
"scanner": "injection_scanner",
|
|
})
|
|
|
|
# Quick scan check
|
|
quick_score = quick_report.get("score", 0)
|
|
quick_count = quick_report.get("total_findings", 0)
|
|
checklist.append({
|
|
"check": "No dangerous code patterns",
|
|
"status": "PASS" if quick_score >= 80 else ("WARN" if quick_score >= 50 else "FAIL"),
|
|
"details": f"{quick_count} finding(s), score={quick_score}",
|
|
"scanner": "quick_scan",
|
|
})
|
|
|
|
# Summary counts
|
|
pass_count = sum(1 for c in checklist if c["status"] == "PASS")
|
|
warn_count = sum(1 for c in checklist if c["status"] == "WARN")
|
|
fail_count = sum(1 for c in checklist if c["status"] == "FAIL")
|
|
|
|
return {
|
|
"checklist": checklist,
|
|
"summary": {
|
|
"pass": pass_count,
|
|
"warn": warn_count,
|
|
"fail": fail_count,
|
|
"total": len(checklist),
|
|
},
|
|
}
|
|
|
|
|
|
def _phase4_red_team_scenarios(all_findings: list[dict], auth_score: float) -> dict:
|
|
"""Phase 4: Red Team Scenarios -- generate attack scenarios from findings."""
|
|
logger.info("Phase 4: Red Team Scenarios")
|
|
|
|
scenarios: list[dict] = []
|
|
seen_types: set[str] = set()
|
|
|
|
# Generate scenarios from findings (one per unique type+file combination,
|
|
# capped to keep the report manageable)
|
|
MAX_SCENARIOS = 20
|
|
|
|
# Sort by severity so we get the most critical first
|
|
severity_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
|
|
sorted_findings = sorted(
|
|
all_findings,
|
|
key=lambda f: severity_order.get(f.get("severity", "INFO"), 5),
|
|
)
|
|
|
|
for finding in sorted_findings:
|
|
if len(scenarios) >= MAX_SCENARIOS:
|
|
break
|
|
|
|
# Determine the template key
|
|
finding_type = finding.get("type", "")
|
|
injection_type = finding.get("injection_type", "")
|
|
pattern = finding.get("pattern", "unknown")
|
|
file_path = finding.get("file", "unknown")
|
|
|
|
# Choose best template
|
|
if injection_type and injection_type in _RED_TEAM_TEMPLATES:
|
|
template_key = injection_type
|
|
elif finding_type in _RED_TEAM_TEMPLATES:
|
|
template_key = finding_type
|
|
else:
|
|
template_key = None
|
|
|
|
template = (
|
|
_RED_TEAM_TEMPLATES.get(template_key, _RED_TEAM_FALLBACK)
|
|
if template_key
|
|
else _RED_TEAM_FALLBACK
|
|
)
|
|
|
|
# Deduplicate: one scenario per (template_key, file) pair
|
|
dedup_key = f"{template_key or finding_type}:{file_path}"
|
|
if dedup_key in seen_types:
|
|
continue
|
|
seen_types.add(dedup_key)
|
|
|
|
# Interpolate template
|
|
scenario_text = template["scenario"].format(
|
|
pattern=pattern,
|
|
file=file_path,
|
|
)
|
|
|
|
scenarios.append({
|
|
"title": template["title"],
|
|
"persona": template["persona"],
|
|
"scenario": scenario_text,
|
|
"impact": template["impact"],
|
|
"difficulty": template["difficulty"],
|
|
"severity": finding.get("severity", "MEDIUM"),
|
|
"source_finding": {
|
|
"type": finding_type,
|
|
"pattern": pattern,
|
|
"file": file_path,
|
|
"line": finding.get("line", 0),
|
|
},
|
|
})
|
|
|
|
# Add no-auth scenario if auth score is low
|
|
if auth_score < 40 and "no_auth" not in seen_types:
|
|
template = _RED_TEAM_TEMPLATES["no_auth"]
|
|
scenarios.append({
|
|
"title": template["title"],
|
|
"persona": template["persona"],
|
|
"scenario": template["scenario"],
|
|
"impact": template["impact"],
|
|
"difficulty": template["difficulty"],
|
|
"severity": "HIGH",
|
|
"source_finding": {
|
|
"type": "architectural",
|
|
"pattern": "missing_auth",
|
|
"file": "(project-wide)",
|
|
"line": 0,
|
|
},
|
|
})
|
|
|
|
return {
|
|
"scenarios": scenarios,
|
|
"total_scenarios": len(scenarios),
|
|
}
|
|
|
|
|
|
def _phase5_blue_team_recommendations(all_findings: list[dict], auth_score: float) -> dict:
|
|
"""Phase 5: Blue Team Recommendations -- hardening advice per finding type."""
|
|
logger.info("Phase 5: Blue Team Recommendations")
|
|
|
|
recommendations: list[dict] = []
|
|
seen_types: set[str] = set()
|
|
|
|
# Group findings by type for consolidated recommendations
|
|
for finding in all_findings:
|
|
finding_type = finding.get("type", "")
|
|
injection_type = finding.get("injection_type", "")
|
|
|
|
# Choose best template key
|
|
if injection_type and injection_type in _BLUE_TEAM_TEMPLATES:
|
|
rec_key = injection_type
|
|
elif finding_type in _BLUE_TEAM_TEMPLATES:
|
|
rec_key = finding_type
|
|
else:
|
|
rec_key = None
|
|
|
|
if rec_key and rec_key not in seen_types:
|
|
seen_types.add(rec_key)
|
|
template = _BLUE_TEAM_TEMPLATES[rec_key]
|
|
|
|
# Count affected findings
|
|
affected = [
|
|
f for f in all_findings
|
|
if f.get("injection_type", "") == rec_key
|
|
or f.get("type", "") == rec_key
|
|
]
|
|
|
|
recommendations.append({
|
|
"category": rec_key,
|
|
"recommendation": template["recommendation"],
|
|
"priority": template["priority"],
|
|
"effort": template["effort"],
|
|
"affected_findings": len(affected),
|
|
"example_files": sorted(set(
|
|
f.get("file", "") for f in affected[:5]
|
|
)),
|
|
})
|
|
|
|
# Add no-auth recommendation if applicable
|
|
if auth_score < 40 and "no_auth" not in seen_types:
|
|
template = _BLUE_TEAM_TEMPLATES["no_auth"]
|
|
recommendations.append({
|
|
"category": "no_auth",
|
|
"recommendation": template["recommendation"],
|
|
"priority": template["priority"],
|
|
"effort": template["effort"],
|
|
"affected_findings": 0,
|
|
"example_files": [],
|
|
})
|
|
|
|
# Sort by priority (CRITICAL first)
|
|
priority_order = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
|
|
recommendations.sort(key=lambda r: priority_order.get(r["priority"], 5))
|
|
|
|
return {
|
|
"recommendations": recommendations,
|
|
"total_recommendations": len(recommendations),
|
|
}
|
|
|
|
|
|
def _phase6_verdict(
|
|
target_str: str,
|
|
all_findings: list[dict],
|
|
source_files: list[Path],
|
|
total_source_files: int,
|
|
secrets_report: dict,
|
|
dep_report: dict,
|
|
inj_report: dict,
|
|
quick_report: dict,
|
|
) -> dict:
|
|
"""Phase 6: Verdict -- compute score and emit final verdict."""
|
|
logger.info("Phase 6: Verdict")
|
|
|
|
domain_scores = score_calculator.compute_domain_scores(
|
|
secrets_findings=secrets_report.get("findings", []),
|
|
injection_findings=inj_report.get("findings", []),
|
|
dependency_report=dep_report,
|
|
quick_findings=quick_report.get("findings", []),
|
|
source_files=source_files,
|
|
total_source_files=total_source_files,
|
|
)
|
|
|
|
final_score = calculate_weighted_score(domain_scores)
|
|
verdict = get_verdict(final_score)
|
|
|
|
return {
|
|
"domain_scores": domain_scores,
|
|
"final_score": final_score,
|
|
"verdict": {
|
|
"label": verdict["label"],
|
|
"description": verdict["description"],
|
|
"emoji": verdict["emoji"],
|
|
},
|
|
}
|
|
|
|
|
|
# =========================================================================
|
|
# REPORT GENERATION
|
|
# =========================================================================
|
|
|
|
def _generate_markdown_report(
|
|
target: str,
|
|
phases: dict,
|
|
elapsed: float,
|
|
phases_run: list[int],
|
|
) -> str:
|
|
"""Generate a comprehensive Markdown audit report."""
|
|
lines: list[str] = []
|
|
ts = get_timestamp()
|
|
|
|
lines.append("# 007 -- Full Security Audit Report")
|
|
lines.append("")
|
|
lines.append(f"**Target:** `{target}`")
|
|
lines.append(f"**Timestamp:** {ts}")
|
|
lines.append(f"**Duration:** {elapsed:.2f}s")
|
|
lines.append(f"**Phases executed:** {', '.join(str(p) for p in phases_run)}")
|
|
lines.append("")
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 1: Surface Mapping
|
|
if 1 in phases_run and "phase1" in phases:
|
|
p1 = phases["phase1"]
|
|
lines.append("## Phase 1: Surface Mapping")
|
|
lines.append("")
|
|
lines.append(f"**Total files:** {p1.get('total_files', 0)}")
|
|
lines.append("")
|
|
|
|
# Files by type
|
|
fbt = p1.get("files_by_type", {})
|
|
if fbt:
|
|
lines.append("### File Types")
|
|
lines.append("")
|
|
lines.append("| Extension | Count |")
|
|
lines.append("|-----------|-------|")
|
|
for ext, count in list(fbt.items())[:20]:
|
|
lines.append(f"| `{ext}` | {count} |")
|
|
lines.append("")
|
|
|
|
# Entry points
|
|
eps = p1.get("entry_points", [])
|
|
if eps:
|
|
lines.append("### Entry Points")
|
|
lines.append("")
|
|
for ep in eps:
|
|
lines.append(f"- `{ep}`")
|
|
lines.append("")
|
|
|
|
# Dependency files
|
|
dfs = p1.get("dependency_files", [])
|
|
if dfs:
|
|
lines.append("### Dependency Files")
|
|
lines.append("")
|
|
for df in dfs:
|
|
lines.append(f"- `{df}`")
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 2: Threat Modeling Hints
|
|
if 2 in phases_run and "phase2" in phases:
|
|
p2 = phases["phase2"]
|
|
lines.append("## Phase 2: Threat Modeling Hints")
|
|
lines.append("")
|
|
lines.append(f"**Components identified for STRIDE analysis:** {p2.get('total_components', 0)}")
|
|
lines.append("")
|
|
|
|
for comp in p2.get("components_for_stride", [])[:30]:
|
|
lines.append(f"- **`{comp['component']}`** ({comp['type']})")
|
|
lines.append(f" - STRIDE focus: {', '.join(comp['stride_focus'])}")
|
|
lines.append(f" - Reason: {comp['reason']}")
|
|
lines.append("")
|
|
lines.append(f"> {p2.get('recommendation', '')}")
|
|
lines.append("")
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 3: Security Checklist
|
|
if 3 in phases_run and "phase3" in phases:
|
|
p3 = phases["phase3"]
|
|
summary = p3.get("summary", {})
|
|
lines.append("## Phase 3: Security Checklist")
|
|
lines.append("")
|
|
lines.append(
|
|
f"**Results:** {summary.get('pass', 0)} PASS / "
|
|
f"{summary.get('warn', 0)} WARN / "
|
|
f"{summary.get('fail', 0)} FAIL"
|
|
)
|
|
lines.append("")
|
|
lines.append("| Check | Status | Details | Scanner |")
|
|
lines.append("|-------|--------|---------|---------|")
|
|
for item in p3.get("checklist", []):
|
|
status_icon = {"PASS": "[PASS]", "WARN": "[WARN]", "FAIL": "[FAIL]"}.get(
|
|
item["status"], item["status"]
|
|
)
|
|
lines.append(
|
|
f"| {item['check']} | {status_icon} | {item['details']} | {item['scanner']} |"
|
|
)
|
|
lines.append("")
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 4: Red Team Scenarios
|
|
if 4 in phases_run and "phase4" in phases:
|
|
p4 = phases["phase4"]
|
|
lines.append("## Phase 4: Red Team Scenarios")
|
|
lines.append("")
|
|
lines.append(f"**Total scenarios:** {p4.get('total_scenarios', 0)}")
|
|
lines.append("")
|
|
|
|
for i, sc in enumerate(p4.get("scenarios", []), start=1):
|
|
lines.append(f"### Scenario {i}: {sc['title']}")
|
|
lines.append("")
|
|
lines.append(f"- **Persona:** {sc['persona']}")
|
|
lines.append(f"- **Severity:** {sc['severity']}")
|
|
lines.append(f"- **Difficulty:** {sc['difficulty']}")
|
|
lines.append(f"- **Impact:** {sc['impact']}")
|
|
lines.append(f"- **Description:** {sc['scenario']}")
|
|
src = sc.get("source_finding", {})
|
|
if src.get("file"):
|
|
lines.append(f"- **Source:** `{src['file']}`:L{src.get('line', 0)} ({src.get('pattern', '')})")
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 5: Blue Team Recommendations
|
|
if 5 in phases_run and "phase5" in phases:
|
|
p5 = phases["phase5"]
|
|
lines.append("## Phase 5: Blue Team Recommendations")
|
|
lines.append("")
|
|
lines.append(f"**Total recommendations:** {p5.get('total_recommendations', 0)}")
|
|
lines.append("")
|
|
|
|
for rec in p5.get("recommendations", []):
|
|
lines.append(f"### [{rec['priority']}] {rec['category'].replace('_', ' ').title()}")
|
|
lines.append("")
|
|
lines.append(f"**Affected findings:** {rec['affected_findings']}")
|
|
lines.append(f"**Effort:** {rec['effort']}")
|
|
lines.append("")
|
|
lines.append(f"{rec['recommendation']}")
|
|
lines.append("")
|
|
if rec.get("example_files"):
|
|
lines.append("**Example files:**")
|
|
for ef in rec["example_files"]:
|
|
if ef:
|
|
lines.append(f"- `{ef}`")
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# Phase 6: Verdict
|
|
if 6 in phases_run and "phase6" in phases:
|
|
p6 = phases["phase6"]
|
|
domain_scores = p6.get("domain_scores", {})
|
|
final_score = p6.get("final_score", 0)
|
|
verdict = p6.get("verdict", {})
|
|
|
|
lines.append("## Phase 6: Verdict")
|
|
lines.append("")
|
|
lines.append("### Domain Scores")
|
|
lines.append("")
|
|
lines.append("| Domain | Weight | Score |")
|
|
lines.append("|--------|--------|-------|")
|
|
for domain, weight in SCORING_WEIGHTS.items():
|
|
score = domain_scores.get(domain, 0.0)
|
|
label = SCORING_LABELS.get(domain, domain)
|
|
lines.append(f"| {label} | {weight * 100:.0f}% | {score:.1f} |")
|
|
lines.append("")
|
|
|
|
lines.append(f"### Final Score: **{final_score:.1f} / 100**")
|
|
lines.append("")
|
|
lines.append(
|
|
f"### Verdict: **{verdict.get('emoji', '')} {verdict.get('label', 'N/A')}**"
|
|
)
|
|
lines.append("")
|
|
lines.append(f"> {verdict.get('description', '')}")
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
lines.append("*Generated by 007 -- Licenca para Auditar*")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _generate_text_summary(
|
|
target: str,
|
|
phases: dict,
|
|
elapsed: float,
|
|
phases_run: list[int],
|
|
) -> str:
|
|
"""Generate a concise text summary for stdout."""
|
|
lines: list[str] = []
|
|
|
|
lines.append("=" * 72)
|
|
lines.append(" 007 FULL SECURITY AUDIT -- SUMMARY")
|
|
lines.append("=" * 72)
|
|
lines.append("")
|
|
lines.append(f" Target: {target}")
|
|
lines.append(f" Timestamp: {get_timestamp()}")
|
|
lines.append(f" Duration: {elapsed:.2f}s")
|
|
lines.append(f" Phases: {', '.join(str(p) for p in phases_run)}")
|
|
lines.append("")
|
|
|
|
# Phase 1 summary
|
|
if "phase1" in phases:
|
|
p1 = phases["phase1"]
|
|
lines.append(f" Phase 1 -- Surface: {p1.get('total_files', 0)} files, "
|
|
f"{len(p1.get('entry_points', []))} entry points, "
|
|
f"{len(p1.get('dependency_files', []))} dep files")
|
|
|
|
# Phase 2 summary
|
|
if "phase2" in phases:
|
|
p2 = phases["phase2"]
|
|
lines.append(f" Phase 2 -- Threat Model Hints: "
|
|
f"{p2.get('total_components', 0)} components for STRIDE")
|
|
|
|
# Phase 3 summary
|
|
if "phase3" in phases:
|
|
p3 = phases["phase3"]
|
|
summary = p3.get("summary", {})
|
|
lines.append(
|
|
f" Phase 3 -- Checklist: "
|
|
f"{summary.get('pass', 0)} PASS / "
|
|
f"{summary.get('warn', 0)} WARN / "
|
|
f"{summary.get('fail', 0)} FAIL"
|
|
)
|
|
|
|
# Phase 4 summary
|
|
if "phase4" in phases:
|
|
p4 = phases["phase4"]
|
|
lines.append(f" Phase 4 -- Red Team: {p4.get('total_scenarios', 0)} attack scenarios")
|
|
|
|
# Phase 5 summary
|
|
if "phase5" in phases:
|
|
p5 = phases["phase5"]
|
|
lines.append(f" Phase 5 -- Blue Team: {p5.get('total_recommendations', 0)} recommendations")
|
|
|
|
# Phase 6 verdict
|
|
if "phase6" in phases:
|
|
p6 = phases["phase6"]
|
|
final_score = p6.get("final_score", 0)
|
|
verdict = p6.get("verdict", {})
|
|
lines.append("")
|
|
lines.append("-" * 72)
|
|
lines.append(f" FINAL SCORE: {final_score:.1f} / 100")
|
|
lines.append(f" VERDICT: {verdict.get('emoji', '')} {verdict.get('label', 'N/A')}")
|
|
lines.append(f" {verdict.get('description', '')}")
|
|
|
|
lines.append("=" * 72)
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
# =========================================================================
|
|
# MAIN ENTRY POINT
|
|
# =========================================================================
|
|
|
|
def run_audit(
|
|
target_path: str,
|
|
output_format: str = "text",
|
|
phases_to_run: str = "all",
|
|
verbose: bool = False,
|
|
) -> dict:
|
|
"""Execute the full 6-phase security audit.
|
|
|
|
Args:
|
|
target_path: Path to the directory to audit.
|
|
output_format: 'text', 'json', or 'markdown'.
|
|
phases_to_run: 'all' or a comma-separated list of phase numbers (e.g. '1,3,6').
|
|
verbose: Enable debug-level logging.
|
|
|
|
Returns:
|
|
JSON-compatible audit report dict.
|
|
"""
|
|
if verbose:
|
|
logger.setLevel("DEBUG")
|
|
|
|
ensure_directories()
|
|
|
|
target = Path(target_path).resolve()
|
|
if not target.exists():
|
|
logger.error("Target path does not exist: %s", target)
|
|
sys.exit(1)
|
|
if not target.is_dir():
|
|
logger.error("Target is not a directory: %s", target)
|
|
sys.exit(1)
|
|
|
|
# Parse phases
|
|
if phases_to_run == "all":
|
|
phases_list = [1, 2, 3, 4, 5, 6]
|
|
else:
|
|
try:
|
|
phases_list = sorted(set(int(p.strip()) for p in phases_to_run.split(",")))
|
|
if not all(1 <= p <= 6 for p in phases_list):
|
|
logger.error("Phase numbers must be between 1 and 6.")
|
|
sys.exit(1)
|
|
except ValueError:
|
|
logger.error("Invalid --phase value. Use 'all' or comma-separated numbers (1-6).")
|
|
sys.exit(1)
|
|
|
|
logger.info("Starting full audit of %s (phases: %s)", target, phases_list)
|
|
start_time = time.time()
|
|
target_str = str(target)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Run scanners if needed (phases 3-6 need scanner data)
|
|
# ------------------------------------------------------------------
|
|
need_scanners = any(p in phases_list for p in [3, 4, 5, 6])
|
|
|
|
secrets_report: dict = {"findings": [], "score": 100, "total_findings": 0}
|
|
dep_report: dict = {"findings": [], "score": 100, "total_findings": 0}
|
|
inj_report: dict = {"findings": [], "score": 100, "total_findings": 0}
|
|
quick_report: dict = {"findings": [], "score": 100, "total_findings": 0}
|
|
all_findings: list[dict] = []
|
|
report_findings: list[dict] = []
|
|
|
|
if need_scanners:
|
|
logger.info("Running scanners for phases %s...", [p for p in phases_list if p >= 3])
|
|
|
|
try:
|
|
secrets_report = secrets_scanner.run_scan(
|
|
target_path=target_str, output_format="json", verbose=verbose,
|
|
)
|
|
except SystemExit:
|
|
pass
|
|
|
|
try:
|
|
dep_report = dependency_scanner.run_scan(
|
|
target_path=target_str, output_format="json", verbose=verbose,
|
|
)
|
|
except SystemExit:
|
|
pass
|
|
|
|
try:
|
|
inj_report = injection_scanner.run_scan(
|
|
target_path=target_str, output_format="json", verbose=verbose,
|
|
)
|
|
except SystemExit:
|
|
pass
|
|
|
|
try:
|
|
quick_report = quick_scan.run_scan(
|
|
target_path=target_str, output_format="json", verbose=verbose,
|
|
)
|
|
except SystemExit:
|
|
pass
|
|
|
|
# Aggregate and deduplicate
|
|
raw = (
|
|
secrets_report.get("findings", [])
|
|
+ dep_report.get("findings", [])
|
|
+ inj_report.get("findings", [])
|
|
+ quick_report.get("findings", [])
|
|
)
|
|
all_findings = score_calculator._deduplicate_findings(raw)
|
|
report_findings = score_calculator.redact_findings_for_report(all_findings)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Collect source files if needed for phase 6
|
|
# ------------------------------------------------------------------
|
|
source_files: list[Path] = []
|
|
total_source_files = 0
|
|
if 6 in phases_list:
|
|
source_files = score_calculator._collect_source_files(target)
|
|
total_source_files = len(source_files)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Execute phases
|
|
# ------------------------------------------------------------------
|
|
phases_data: dict = {}
|
|
|
|
if 1 in phases_list:
|
|
phases_data["phase1"] = _phase1_surface_mapping(target, verbose=verbose)
|
|
|
|
if 2 in phases_list:
|
|
# Phase 2 benefits from phase 1 data and findings
|
|
surface = phases_data.get("phase1") or _phase1_surface_mapping(target, verbose=verbose)
|
|
phases_data["phase2"] = _phase2_threat_modeling_hints(surface, report_findings)
|
|
|
|
if 3 in phases_list:
|
|
phases_data["phase3"] = _phase3_security_checklist(
|
|
secrets_report, dep_report, inj_report, quick_report,
|
|
)
|
|
|
|
# Auth score for phases 4 and 5
|
|
auth_score = 50.0
|
|
if 6 in phases_list or 4 in phases_list or 5 in phases_list:
|
|
if source_files:
|
|
auth_count = score_calculator._count_pattern_matches(
|
|
source_files, score_calculator._AUTH_PATTERNS,
|
|
)
|
|
if auth_count == 0:
|
|
auth_score = 25.0
|
|
else:
|
|
auth_score = score_calculator._score_from_positive_signals(
|
|
auth_count, total_source_files, base_score=40, max_score=95,
|
|
)
|
|
|
|
if 4 in phases_list:
|
|
phases_data["phase4"] = _phase4_red_team_scenarios(report_findings, auth_score)
|
|
|
|
if 5 in phases_list:
|
|
phases_data["phase5"] = _phase5_blue_team_recommendations(report_findings, auth_score)
|
|
|
|
if 6 in phases_list:
|
|
phases_data["phase6"] = _phase6_verdict(
|
|
target_str=target_str,
|
|
all_findings=all_findings,
|
|
source_files=source_files,
|
|
total_source_files=total_source_files,
|
|
secrets_report=secrets_report,
|
|
dep_report=dep_report,
|
|
inj_report=inj_report,
|
|
quick_report=quick_report,
|
|
)
|
|
|
|
elapsed = time.time() - start_time
|
|
|
|
# ------------------------------------------------------------------
|
|
# Generate and save Markdown report
|
|
# ------------------------------------------------------------------
|
|
md_report = _generate_markdown_report(target_str, phases_data, elapsed, phases_list)
|
|
|
|
ts_file = datetime.now(timezone.utc).strftime("%Y-%m-%d_%H-%M-%S")
|
|
report_filename = f"audit_{ts_file}.md"
|
|
report_path = REPORTS_DIR / report_filename
|
|
|
|
try:
|
|
report_path.write_text(md_report, encoding="utf-8")
|
|
logger.info("Markdown report saved to %s", report_path)
|
|
except OSError as exc:
|
|
logger.warning("Could not save report: %s", exc)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Audit log
|
|
# ------------------------------------------------------------------
|
|
verdict_data = phases_data.get("phase6", {}).get("verdict", {})
|
|
final_score = phases_data.get("phase6", {}).get("final_score", "N/A")
|
|
|
|
log_audit_event(
|
|
action="full_audit",
|
|
target=target_str,
|
|
result=f"score={final_score}, verdict={verdict_data.get('label', 'N/A')}",
|
|
details={
|
|
"phases_run": phases_list,
|
|
"total_findings": len(all_findings),
|
|
"report_path": str(report_path),
|
|
"duration_seconds": round(elapsed, 3),
|
|
},
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
# Build final report dict
|
|
# ------------------------------------------------------------------
|
|
full_report = {
|
|
"report": "full_audit",
|
|
"target": target_str,
|
|
"timestamp": get_timestamp(),
|
|
"duration_seconds": round(elapsed, 3),
|
|
"phases_run": phases_list,
|
|
"phases": phases_data,
|
|
"total_findings": len(all_findings),
|
|
"findings": report_findings,
|
|
"report_path": str(report_path),
|
|
}
|
|
|
|
# ------------------------------------------------------------------
|
|
# Output
|
|
# ------------------------------------------------------------------
|
|
if output_format == "json":
|
|
print(json.dumps(full_report, indent=2, ensure_ascii=False))
|
|
elif output_format == "markdown":
|
|
print(md_report)
|
|
else:
|
|
print(_generate_text_summary(target_str, phases_data, elapsed, phases_list))
|
|
print(f" Full report saved to: {report_path}")
|
|
print("")
|
|
|
|
return full_report
|
|
|
|
|
|
# =========================================================================
|
|
# CLI
|
|
# =========================================================================
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"007 Full Audit -- Comprehensive 6-phase security audit.\n\n"
|
|
"Phases:\n"
|
|
" 1: Surface Mapping -- file inventory, entry points, deps\n"
|
|
" 2: Threat Modeling Hints -- STRIDE analysis targets\n"
|
|
" 3: Security Checklist -- run all scanners\n"
|
|
" 4: Red Team Scenarios -- attack scenario generation\n"
|
|
" 5: Blue Team Recs -- hardening recommendations\n"
|
|
" 6: Verdict -- scoring and final verdict"
|
|
),
|
|
epilog=(
|
|
"Examples:\n"
|
|
" python full_audit.py --target ./my-project\n"
|
|
" python full_audit.py --target ./my-project --output markdown\n"
|
|
" python full_audit.py --target ./my-project --phase 1,3,6\n"
|
|
" python full_audit.py --target ./my-project --output json --verbose"
|
|
),
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
)
|
|
parser.add_argument(
|
|
"--target",
|
|
required=True,
|
|
help="Path to the directory to audit (required).",
|
|
)
|
|
parser.add_argument(
|
|
"--output",
|
|
choices=["text", "json", "markdown"],
|
|
default="text",
|
|
help="Output format: 'text' (default), 'json', or 'markdown'.",
|
|
)
|
|
parser.add_argument(
|
|
"--phase",
|
|
default="all",
|
|
help=(
|
|
"Which phases to run: 'all' (default) or comma-separated numbers "
|
|
"(e.g. '1,3,6'). Range: 1-6."
|
|
),
|
|
)
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
default=False,
|
|
help="Enable verbose/debug logging.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
run_audit(
|
|
target_path=args.target,
|
|
output_format=args.output,
|
|
phases_to_run=args.phase,
|
|
verbose=args.verbose,
|
|
)
|