| """ |
| Vulnerability placement engine for security audit scenarios. |
| |
| Selects vulnerability types from the knowledge-base catalog, assigns them to |
| specific hosts/endpoints in the generated topology, and produces instances |
| that conform to the canonical vulnerability schema used by ``scenarios.py``. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import math |
| import random |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| from ..knowledge_base.vulnerabilities import get_vuln_types, get_vuln_types_for_role |
| from ..knowledge_base.compliance import get_controls_for_vuln |
|
|
|
|
| |
| |
| |
|
|
| _DIFFICULTY_PARAMS = { |
| "easy": { |
| "vuln_count": (3, 3), |
| "chain_count": (0, 0), |
| "prefix": "E", |
| }, |
| "medium": { |
| "vuln_count": (5, 7), |
| "chain_count": (1, 2), |
| "prefix": "M", |
| }, |
| "hard": { |
| "vuln_count": (8, 12), |
| "chain_count": (2, 4), |
| "prefix": "H", |
| }, |
| } |
|
|
| |
| _SEVERITY_SCALE = [ |
| (0.0, "None"), |
| (0.1, "Low"), |
| (4.0, "Medium"), |
| (7.0, "High"), |
| (9.0, "Critical"), |
| ] |
|
|
| |
| _FRAMEWORKS = ["PCI-DSS", "SOC2", "HIPAA", "Generic"] |
|
|
|
|
| |
| |
| |
|
|
| def _cvss_to_severity(cvss: float) -> str: |
| """Map a CVSS score to a qualitative severity string.""" |
| severity = "Low" |
| for threshold, label in _SEVERITY_SCALE: |
| if cvss >= threshold: |
| severity = label |
| return severity |
|
|
|
|
| def _role_keyword(role: str) -> str: |
| """Extract a canonical keyword from a free-form role string.""" |
| lower = role.lower() |
| for kw in ("ci/cd", "database", "mail", "file", "monitoring", "api", "web"): |
| if kw in lower: |
| return kw |
| if "application" in lower: |
| return "web" |
| return "web" |
|
|
|
|
| def _generate_evidence(vt: Any, host_ip: str, endpoint: Optional[str], |
| param: Optional[str], rng: random.Random) -> str: |
| """Produce a human-readable evidence string from a VulnType.""" |
| parts: List[str] = [] |
|
|
| if param: |
| parts.append(f"Parameter '{param}' is vulnerable to {vt.name}.") |
| elif endpoint: |
| parts.append(f"Endpoint '{endpoint}' on {host_ip} is vulnerable to {vt.name}.") |
| else: |
| parts.append(f"Host {host_ip} is vulnerable to {vt.name}.") |
|
|
| |
| desc_sentences = vt.description.split(".") |
| if desc_sentences: |
| parts.append(desc_sentences[0].strip() + ".") |
|
|
| return " ".join(parts) |
|
|
|
|
| def _pick_param_for_endpoint(endpoint: Dict, rng: random.Random) -> Optional[str]: |
| """Return a parameter name from an endpoint's param list, or None.""" |
| params = endpoint.get("params", []) |
| if not params: |
| return None |
| return rng.choice(params) |
|
|
|
|
| def _pick_cwe(vt: Any, rng: random.Random) -> str: |
| """Pick one CWE-ID string from the VulnType's list.""" |
| cwe_id = rng.choice(vt.cwe_ids) |
| return f"CWE-{cwe_id}" |
|
|
|
|
| def _pick_cvss(vt: Any, rng: random.Random) -> float: |
| """Generate a CVSS score within the VulnType's range.""" |
| lo, hi = vt.cvss_range |
| |
| return round(lo + rng.random() * (hi - lo), 1) |
|
|
|
|
| def _pick_compliance_controls(vt: Any, rng: random.Random) -> List[str]: |
| """Return compliance controls for this vuln type from the KB.""" |
| framework = rng.choice(_FRAMEWORKS) |
| controls = get_controls_for_vuln(vt.id, framework) |
| if not controls: |
| |
| controls = get_controls_for_vuln(vt.id, "Generic") |
| return controls if controls else [f"{vt.owasp_category}"] |
|
|
|
|
| |
| |
| |
|
|
| def place_vulnerabilities( |
| hosts: Dict, |
| ports: Dict, |
| endpoints: Dict, |
| difficulty: str, |
| rng: random.Random, |
| vuln_catalog: Optional[Dict] = None, |
| ) -> List[Dict]: |
| """Place vulnerability instances onto the generated topology. |
| |
| Parameters |
| ---------- |
| hosts : dict |
| ``{ip: {hostname, os, role, hidden_until?, is_honeypot?}}`` |
| ports : dict |
| ``{ip: [{port, service, version, state}, ...]}`` |
| endpoints : dict |
| ``{ip: [{path, method, description, params}, ...]}`` |
| difficulty : str |
| ``"easy"`` | ``"medium"`` | ``"hard"`` |
| rng : random.Random |
| Seeded RNG for deterministic output. |
| vuln_catalog : dict, optional |
| Override for ``get_vuln_types()``; mainly for testing. |
| |
| Returns |
| ------- |
| list[dict] |
| Vulnerability instances in the canonical schema. |
| """ |
| params = _DIFFICULTY_PARAMS[difficulty] |
| prefix = params["prefix"] |
| total_vulns = rng.randint(*params["vuln_count"]) |
| chain_count = rng.randint(*params["chain_count"]) |
|
|
| catalog = vuln_catalog or get_vuln_types() |
|
|
| |
| visible_ips: List[str] = [] |
| hidden_ips: List[str] = [] |
| honeypot_ips: List[str] = [] |
|
|
| for ip, info in hosts.items(): |
| if info.get("is_honeypot"): |
| honeypot_ips.append(ip) |
| elif "hidden_until" in info: |
| hidden_ips.append(ip) |
| else: |
| visible_ips.append(ip) |
|
|
| |
| visible_ips.sort() |
| hidden_ips.sort() |
| honeypot_ips.sort() |
|
|
| |
| if not visible_ips: |
| raise ValueError("Topology must have at least one visible host.") |
|
|
| |
| |
| chain_count = min(chain_count, len(hidden_ips)) |
| |
| independent_count = max(0, total_vulns - chain_count * 2) |
|
|
| vulns: List[Dict] = [] |
| vuln_index = 1 |
|
|
| |
| gateway_vuln_ids: List[str] = [] |
| for chain_idx in range(chain_count): |
| gateway_ip = visible_ips[chain_idx % len(visible_ips)] |
| hidden_ip = hidden_ips[chain_idx] |
| role = hosts[gateway_ip]["role"] |
|
|
| |
| role_vulns = get_vuln_types_for_role(role) |
| chain_candidates = [v for v in role_vulns if v.chain_potential] |
| if not chain_candidates: |
| chain_candidates = [v for v in catalog.values() if v.chain_potential] |
| vt = rng.choice(chain_candidates) |
|
|
| vuln_id = f"VULN-{prefix}{vuln_index:03d}" |
| vuln_index += 1 |
|
|
| |
| host_eps = endpoints.get(gateway_ip, []) |
| endpoint_path = None |
| param = None |
| if vt.requires_params and host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
| param = _pick_param_for_endpoint(ep, rng) |
| elif host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
|
|
| cvss = _pick_cvss(vt, rng) |
|
|
| vuln_dict = { |
| "id": vuln_id, |
| "host": gateway_ip, |
| "endpoint": endpoint_path, |
| "type": vt.name, |
| "cwe": _pick_cwe(vt, rng), |
| "owasp": vt.owasp_category, |
| "cvss": cvss, |
| "severity": _cvss_to_severity(cvss), |
| "evidence": _generate_evidence(vt, gateway_ip, endpoint_path, param, rng), |
| "remediation": vt.remediation_template, |
| "discoverable_by": list(vt.discoverable_by), |
| "requires_found": [], |
| "compliance_controls": _pick_compliance_controls(vt, rng), |
| } |
| vulns.append(vuln_dict) |
| gateway_vuln_ids.append(vuln_id) |
|
|
| |
| hosts[hidden_ip]["hidden_until"] = [vuln_id] |
|
|
| |
| for chain_idx in range(chain_count): |
| hidden_ip = hidden_ips[chain_idx] |
| gateway_vuln_id = gateway_vuln_ids[chain_idx] |
| role = hosts[hidden_ip]["role"] |
|
|
| role_vulns = get_vuln_types_for_role(role) |
| if not role_vulns: |
| role_vulns = list(catalog.values()) |
| vt = rng.choice(role_vulns) |
|
|
| vuln_id = f"VULN-{prefix}{vuln_index:03d}" |
| vuln_index += 1 |
|
|
| host_eps = endpoints.get(hidden_ip, []) |
| endpoint_path = None |
| param = None |
| if vt.requires_params and host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
| param = _pick_param_for_endpoint(ep, rng) |
| elif host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
|
|
| cvss = _pick_cvss(vt, rng) |
|
|
| vuln_dict = { |
| "id": vuln_id, |
| "host": hidden_ip, |
| "endpoint": endpoint_path, |
| "type": vt.name, |
| "cwe": _pick_cwe(vt, rng), |
| "owasp": vt.owasp_category, |
| "cvss": cvss, |
| "severity": _cvss_to_severity(cvss), |
| "evidence": _generate_evidence(vt, hidden_ip, endpoint_path, param, rng), |
| "remediation": vt.remediation_template, |
| "discoverable_by": list(vt.discoverable_by), |
| "requires_found": [gateway_vuln_id], |
| "compliance_controls": _pick_compliance_controls(vt, rng), |
| } |
| vulns.append(vuln_dict) |
|
|
| |
| all_real_ips = visible_ips + hidden_ips |
| for _ in range(independent_count): |
| target_ip = rng.choice(all_real_ips) |
| role = hosts[target_ip]["role"] |
|
|
| role_vulns = get_vuln_types_for_role(role) |
| if not role_vulns: |
| role_vulns = list(catalog.values()) |
|
|
| |
| used_types_on_host = {v["type"] for v in vulns if v["host"] == target_ip} |
| candidates = [v for v in role_vulns if v.name not in used_types_on_host] |
| if not candidates: |
| candidates = role_vulns |
| vt = rng.choice(candidates) |
|
|
| vuln_id = f"VULN-{prefix}{vuln_index:03d}" |
| vuln_index += 1 |
|
|
| host_eps = endpoints.get(target_ip, []) |
| endpoint_path = None |
| param = None |
| if vt.requires_params and host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
| param = _pick_param_for_endpoint(ep, rng) |
| elif "web_endpoint" in vt.applicable_to and host_eps: |
| ep = rng.choice(host_eps) |
| endpoint_path = ep["path"] |
| else: |
| endpoint_path = None |
|
|
| cvss = _pick_cvss(vt, rng) |
|
|
| |
| requires: List[str] = [] |
| if target_ip in hidden_ips: |
| idx = hidden_ips.index(target_ip) |
| if idx < len(gateway_vuln_ids): |
| requires = [gateway_vuln_ids[idx]] |
|
|
| vuln_dict = { |
| "id": vuln_id, |
| "host": target_ip, |
| "endpoint": endpoint_path, |
| "type": vt.name, |
| "cwe": _pick_cwe(vt, rng), |
| "owasp": vt.owasp_category, |
| "cvss": cvss, |
| "severity": _cvss_to_severity(cvss), |
| "evidence": _generate_evidence(vt, target_ip, endpoint_path, param, rng), |
| "remediation": vt.remediation_template, |
| "discoverable_by": list(vt.discoverable_by), |
| "requires_found": requires, |
| "compliance_controls": _pick_compliance_controls(vt, rng), |
| } |
| vulns.append(vuln_dict) |
|
|
| return vulns |
|
|