Rhodawk Mythos Agent
mythos: ascend to Mythos-level β multi-agent + probabilistic + advanced tooling + RL + MCP suite + FastAPI
14b1bbe | """ | |
| Attack-graph construction for the Planner. | |
| Nodes = hypotheses or intermediate states (e.g. "leaked-pointer", "RCE"). | |
| Edges = exploitation transitions weighted by the joint probability of the | |
| pair occurring in the same code-base + the cost of the chain. | |
| Falls back to a tiny pure-Python adjacency-list when ``networkx`` is absent | |
| so the orchestrator works in minimal images. | |
| """ | |
| from __future__ import annotations | |
| from typing import Any | |
| try: # pragma: no cover - optional dep | |
| import networkx as nx # type: ignore | |
| except Exception: # noqa: BLE001 | |
| nx = None # type: ignore | |
| # Heuristic compatibility map between vulnerability classes that can be | |
| # plausibly chained together to amplify impact. | |
| _CHAIN_RULES: list[tuple[str, str, float]] = [ | |
| ("CWE-22", "CWE-78", 0.7), # path traversal β command injection | |
| ("CWE-89", "CWE-78", 0.5), # SQLi β RCE via UDF | |
| ("CWE-79", "CWE-352", 0.6), # XSS β CSRF | |
| ("CWE-918", "CWE-502", 0.4), # SSRF β deserialisation | |
| ("CWE-119", "CWE-787", 0.8), # overflow β OOB write | |
| ("CWE-787", "CWE-416", 0.7), # OOB write β UAF | |
| ("CWE-416", "CWE-269", 0.6), # UAF β privesc | |
| ("CWE-287", "CWE-862", 0.5), # auth bypass β missing authz | |
| ] | |
| class AttackGraph: | |
| def __init__(self): | |
| self.nodes: dict[str, dict[str, Any]] = {} | |
| self.edges: list[tuple[str, str, float]] = [] | |
| self._g = nx.DiGraph() if nx is not None else None | |
| def add_hypothesis(self, h: dict[str, Any]) -> None: | |
| cwe = h["cwe"] | |
| self.nodes[cwe] = {**h, "id": cwe} | |
| if self._g is not None: | |
| self._g.add_node(cwe, **h) | |
| def connect(self) -> None: | |
| for src, dst, base_w in _CHAIN_RULES: | |
| if src in self.nodes and dst in self.nodes: | |
| w = base_w * self.nodes[src]["confidence"] * self.nodes[dst]["confidence"] | |
| self.edges.append((src, dst, round(w, 4))) | |
| if self._g is not None: | |
| self._g.add_edge(src, dst, weight=w) | |
| def critical_paths(self, top: int = 3) -> list[list[str]]: | |
| if self._g is None or self._g.number_of_nodes() == 0: | |
| # naive heaviest-edge fallback | |
| sorted_e = sorted(self.edges, key=lambda e: e[2], reverse=True)[:top] | |
| return [list(e[:2]) for e in sorted_e] | |
| paths: list[tuple[float, list[str]]] = [] | |
| for src in self._g.nodes: | |
| for dst in self._g.nodes: | |
| if src == dst: | |
| continue | |
| try: | |
| p = nx.shortest_path(self._g, src, dst, weight=lambda *_: 1) | |
| score = sum(self._g.edges[a, b].get("weight", 0) for a, b in zip(p, p[1:])) | |
| paths.append((score, p)) | |
| except Exception: | |
| continue | |
| paths.sort(key=lambda x: x[0], reverse=True) | |
| return [p for _, p in paths[:top]] | |
| def to_dict(self) -> dict[str, Any]: | |
| return { | |
| "nodes": list(self.nodes.values()), | |
| "edges": [{"src": s, "dst": d, "weight": w} for s, d, w in self.edges], | |
| "critical_paths": self.critical_paths(), | |
| } | |