vulnir 0.1.0

Vulnerability rule and finding primitives used by jsdet
Documentation
//! Minimal stub implementation of vulnir for jsdet-core tests.
//!
//! This provides just enough types to satisfy the imports in jsdet-core.

use std::collections::HashMap;

pub use petgraph::graph::{Graph as VulnIRGraph, NodeIndex};

/// Confidence value for assertions.
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ConfidenceValue(f64);

impl From<f64> for ConfidenceValue {
    fn from(value: f64) -> Self {
        Self(value.clamp(0.0, 1.0))
    }
}

impl ConfidenceValue {
    pub fn value(&self) -> f64 {
        self.0
    }
}

impl Default for ConfidenceValue {
    fn default() -> Self {
        Self(1.0)
    }
}

/// Producer of vulnerability information.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Producer {
    pub name: String,
    pub kind: ProducerKind,
    pub version: Option<String>,
}

impl Producer {
    pub fn new(name: impl Into<String>, kind: ProducerKind) -> Self {
        Self {
            name: name.into(),
            kind,
            version: None,
        }
    }

    pub fn with_version(mut self, version: impl Into<String>) -> Self {
        self.version = Some(version.into());
        self
    }
}

/// Type of producer.
#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize, serde::Deserialize)]
pub enum ProducerKind {
    #[default]
    Static,
    Dynamic,
    Hybrid,
}

/// Provenance information.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct Provenance {
    pub producer: Producer,
    pub metadata: HashMap<String, serde_json::Value>,
    pub location: Option<SourceLocation>,
}

impl Provenance {
    pub fn new(producer: Producer) -> Self {
        Self {
            producer,
            metadata: HashMap::new(),
            location: None,
        }
    }
}

/// A vulnerability graph edge.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum VulnEdge {
    TaintReach {
        path: String,
        confidence: ConfidenceValue,
        observed_dynamically: bool,
        transform_chain: Vec<String>,
        evidence: Vec<Evidence>,
    },
    DataFlow {
        confidence: ConfidenceValue,
    },
    ControlFlow,
}

/// A vulnerability graph node.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum VulnNode {
    TrustBoundary {
        id: String,
        description: String,
        source_type: String,
        confidence: ConfidenceValue,
        taint_id: Option<String>,
        created_ns: Option<u64>,
        provenance: Vec<Provenance>,
        metadata: Metadata,
    },
    Capability {
        id: String,
        resource: String,
        permission: String,
        target: Option<String>,
        arguments: Vec<String>,
        duration_ns: Option<u64>,
        provenance: Vec<Provenance>,
        metadata: Metadata,
    },
    DataSink {
        id: String,
        sink_type: String,
        description: String,
        provenance: Vec<Provenance>,
    },
    Source {
        id: String,
        source_type: String,
        description: String,
        provenance: Vec<Provenance>,
    },
}

/// Metadata for nodes.
pub type Metadata = HashMap<String, serde_json::Value>;

/// Evidence for assertions.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Evidence {
    pub kind: String,
    pub data: serde_json::Value,
    pub confidence: ConfidenceValue,
    pub observed_dynamically: bool,
}

impl Evidence {
    pub fn new(
        kind: impl Into<String>,
        data: impl Into<String>,
        confidence: impl Into<ConfidenceValue>,
        observed_dynamically: bool,
    ) -> Self {
        Self {
            kind: kind.into(),
            data: serde_json::json!(data.into()),
            confidence: confidence.into(),
            observed_dynamically,
        }
    }
}

/// Source location information.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct SourceLocation {
    pub file: Option<String>,
    pub line: Option<u32>,
    pub column: Option<u32>,
    pub artifact: Option<String>,
}

/// Trait for vulnerability producers.
pub trait VulnProducer {
    fn producer(&self) -> Producer;

    fn create_provenance(&self, location: Option<SourceLocation>) -> Provenance {
        let mut prov = Provenance::new(self.producer());
        prov.location = location;
        prov
    }
}

// Re-export petgraph types for graph manipulation
pub use petgraph::Direction;

/// Extension trait for VulnIRGraph.
pub trait VulnIRGraphExt {
    fn nodes(&self) -> impl Iterator<Item = (NodeIndex, &VulnNode)>;
    fn edges(&self) -> impl Iterator<Item = (NodeIndex, NodeIndex, &VulnEdge)>;
    fn node_count(&self) -> usize;
    fn edge_count(&self) -> usize;
    /// Returns nodes that represent attack surfaces (TrustBoundary nodes).
    fn attack_surface(&self) -> Vec<(NodeIndex, &VulnNode)>;
    /// Returns capabilities reachable from a given node.
    fn reachable_capabilities(&self, _start: NodeIndex) -> Vec<(NodeIndex, &VulnNode)>;
}

impl VulnIRGraphExt for VulnIRGraph<VulnNode, VulnEdge> {
    fn nodes(&self) -> impl Iterator<Item = (NodeIndex, &VulnNode)> {
        self.node_indices().map(|idx| (idx, &self[idx]))
    }

    fn edges(&self) -> impl Iterator<Item = (NodeIndex, NodeIndex, &VulnEdge)> {
        self.edge_indices()
            .filter_map(|e| self.edge_endpoints(e).map(|(a, b)| (a, b, &self[e])))
    }

    fn node_count(&self) -> usize {
        self.node_count()
    }

    fn edge_count(&self) -> usize {
        self.edge_count()
    }

    fn attack_surface(&self) -> Vec<(NodeIndex, &VulnNode)> {
        self.node_indices()
            .filter_map(|idx| {
                let node = &self[idx];
                match node {
                    VulnNode::TrustBoundary { .. } => Some((idx, node)),
                    _ => None,
                }
            })
            .collect()
    }

    fn reachable_capabilities(&self, _start: NodeIndex) -> Vec<(NodeIndex, &VulnNode)> {
        self.node_indices()
            .filter_map(|idx| {
                let node = &self[idx];
                match node {
                    VulnNode::Capability { .. } => Some((idx, node)),
                    _ => None,
                }
            })
            .collect()
    }
}

/// Severity levels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Severity {
    Critical,
    High,
    Medium,
    Low,
    Info,
}

/// CWE (Common Weakness Enumeration) identifier.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CweId(pub String);

impl CweId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }
}