//! Graph memory & attack-path reasoning — web and internal network in one graph. //! //! NeuroSploit already keeps three graphs, each good at one thing: //! [`crate::knowledge_graph`] (what was learned about a target, across runs), //! [`crate::internal`] (hosts, identities, credentials and the AD credential //! loop) and [`crate::chain`] (which finding hands which capability to which). //! This layer projects all three into a single typed schema — the one an //! analyst would draw in Neo4j — and reasons over it: //! //! - **local vs chain severity.** A finding has a local severity. A path from it //! to a business impact, a crown-jewel asset or a privileged identity has a //! chain severity, and the chain severity is only *raised* when every step of //! the path is evidenced. An unproven path keeps the local severity and states //! what it would become, and what evidence would prove it. //! - **status.** `confirmed` (every finding confirmed and every edge evidenced), //! `needs_validation` (findings real, a link between them derived or //! unproven) or `hypothesis` (rests on an unconfirmed finding). //! - **decision support.** The chains and the missing evidence become the next //! moves fed to the attack-chaining rounds, so exploration goes where a //! validated edge would complete the highest-value path. //! //! It works with no database at all: projection, reasoning and a Cypher export //! are in-process. When Neo4j is configured (`--neo4j` or `NEO4J_URI`, with //! `NEO4J_USER`/`NEO4J_PASSWORD`), the graph is MERGEd into it through the HTTP //! transactional API with parameterised statements, and paths accumulated across //! runs and engagements are read back into the same reasoning. //! //! Data minimisation is a hard rule here: no raw secret, token, cookie, //! password, response body or narrative ever becomes a graph property. Secrets //! are fingerprints, identities with personal data are masked, endpoints lose //! their query strings, and evidence is referenced by hash. use crate::internal::{InternalGraph, Kind as IKind, Layer as ILayer}; use crate::knowledge_graph::{EdgeKind as KEdge, KnowledgeGraph, NodeKind as KKind}; use crate::types::Finding; use crate::validation::{Evidence, Exchange}; use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Write as _; // --------------------------------------------------------------------------- // Schema /// Node labels. Closed set: labels are never built from data, so a Cypher /// statement can only ever name one of these. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum Label { Target, Asset, Host, IPAddress, Domain, Service, Endpoint, Technology, Vulnerability, Finding, Evidence, Credential, Identity, Role, Permission, Secret, ExploitAttempt, ValidationResult, AttackStep, AttackPath, BusinessImpact, Control, Mitigation, } impl Label { pub const ALL: [Label; 23] = [ Label::Target, Label::Asset, Label::Host, Label::IPAddress, Label::Domain, Label::Service, Label::Endpoint, Label::Technology, Label::Vulnerability, Label::Finding, Label::Evidence, Label::Credential, Label::Identity, Label::Role, Label::Permission, Label::Secret, Label::ExploitAttempt, Label::ValidationResult, Label::AttackStep, Label::AttackPath, Label::BusinessImpact, Label::Control, Label::Mitigation, ]; pub fn as_str(self) -> &'static str { match self { Label::Target => "Target", Label::Asset => "Asset", Label::Host => "Host", Label::IPAddress => "IPAddress", Label::Domain => "Domain", Label::Service => "Service", Label::Endpoint => "Endpoint", Label::Technology => "Technology", Label::Vulnerability => "Vulnerability", Label::Finding => "Finding", Label::Evidence => "Evidence", Label::Credential => "Credential", Label::Identity => "Identity", Label::Role => "Role", Label::Permission => "Permission", Label::Secret => "Secret", Label::ExploitAttempt => "ExploitAttempt", Label::ValidationResult => "ValidationResult", Label::AttackStep => "AttackStep", Label::AttackPath => "AttackPath", Label::BusinessImpact => "BusinessImpact", Label::Control => "Control", Label::Mitigation => "Mitigation", } } pub fn parse(s: &str) -> Option