Vapt-env / server /generator /placement.py
Sayuj63's picture
first commit
344db23
Raw
History Blame Contribute Delete
11.8 kB
"""
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 presets
# ---------------------------------------------------------------------------
_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 thresholds for CVSS (matching NVD v3.1 qualitative scale).
_SEVERITY_SCALE = [
(0.0, "None"),
(0.1, "Low"),
(4.0, "Medium"),
(7.0, "High"),
(9.0, "Critical"),
]
# Compliance frameworks to cycle through.
_FRAMEWORKS = ["PCI-DSS", "SOC2", "HIPAA", "Generic"]
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
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}.")
# Append a trimmed excerpt of the type description.
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
# Round to one decimal.
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:
# Fallback: try Generic.
controls = get_controls_for_vuln(vt.id, "Generic")
return controls if controls else [f"{vt.owasp_category}"]
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
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()
# Separate hosts into visible, hidden, and honeypots.
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)
# Sort for determinism.
visible_ips.sort()
hidden_ips.sort()
honeypot_ips.sort()
# We need at least one visible host.
if not visible_ips:
raise ValueError("Topology must have at least one visible host.")
# --- Plan chain structure ---
# Each chain: one gateway vuln on a visible host -> unlocks a hidden host.
chain_count = min(chain_count, len(hidden_ips))
# Reserve slots: chain_count gateway vulns + chain_count chained vulns.
independent_count = max(0, total_vulns - chain_count * 2)
vulns: List[Dict] = []
vuln_index = 1
# Gateway vulns are placed on visible hosts and unlock hidden hosts.
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"]
# Pick a vuln type with chain_potential=True.
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
# Pick endpoint if applicable.
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)
# Mark the hidden host.
hosts[hidden_ip]["hidden_until"] = [vuln_id]
# --- Chained vulns on hidden hosts ---
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)
# --- Independent vulns on all non-honeypot hosts ---
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())
# Avoid duplicating the exact same vuln type on the same host if possible.
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)
# For vulns on hidden hosts, they require the gateway.
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