Skip to main content

gossan_graph/
schema.rs

1//! Typed graph schema with versioning for forward compatibility.
2
3use serde::{Deserialize, Serialize};
4
5/// Current schema version. Bump on breaking node/edge type changes.
6pub const SCHEMA_VERSION: u32 = 1;
7
8/// All node types in the attack-surface graph.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[non_exhaustive]
11pub enum NodeType {
12    Domain,
13    Subdomain,
14    Ip,
15    Port,
16    Service,
17    Tech,
18    Endpoint,
19    Secret,
20    Cloud,
21    Finding,
22}
23
24impl std::fmt::Display for NodeType {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            NodeType::Domain => write!(f, "domain"),
28            NodeType::Subdomain => write!(f, "subdomain"),
29            NodeType::Ip => write!(f, "ip"),
30            NodeType::Port => write!(f, "port"),
31            NodeType::Service => write!(f, "service"),
32            NodeType::Tech => write!(f, "tech"),
33            NodeType::Endpoint => write!(f, "endpoint"),
34            NodeType::Secret => write!(f, "secret"),
35            NodeType::Cloud => write!(f, "cloud"),
36            NodeType::Finding => write!(f, "finding"),
37        }
38    }
39}
40
41/// All edge (relationship) types in the attack-surface graph.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[non_exhaustive]
44pub enum EdgeType {
45    ResolvesTo,
46    Hosts,
47    Runs,
48    Exposes,
49    Leaks,
50    Misconfigured,
51    HasFinding,
52    HasService,
53}
54
55impl std::fmt::Display for EdgeType {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            EdgeType::ResolvesTo => write!(f, "RESOLVES_TO"),
59            EdgeType::Hosts => write!(f, "HOSTS"),
60            EdgeType::Runs => write!(f, "RUNS"),
61            EdgeType::Exposes => write!(f, "EXPOSES"),
62            EdgeType::Leaks => write!(f, "LEAKS"),
63            EdgeType::Misconfigured => write!(f, "MISCONFIGURED"),
64            EdgeType::HasFinding => write!(f, "HAS_FINDING"),
65            EdgeType::HasService => write!(f, "HAS_SERVICE"),
66        }
67    }
68}
69
70/// Schema metadata attached to every persisted graph.
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
72pub struct GraphSchema {
73    pub version: u32,
74}
75
76impl Default for GraphSchema {
77    fn default() -> Self {
78        Self {
79            version: SCHEMA_VERSION,
80        }
81    }
82}
83
84impl GraphSchema {
85    /// Create the current schema.
86    #[must_use]
87    pub fn current() -> Self {
88        Self::default()
89    }
90
91    /// Validate that a loaded schema is compatible with this code.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the stored schema version is newer than the
96    /// code understands (forward incompatibility).
97    pub fn validate(&self) -> Result<(), SchemaError> {
98        if self.version > SCHEMA_VERSION {
99            return Err(SchemaError::UnsupportedVersion {
100                found: self.version,
101                max_supported: SCHEMA_VERSION,
102            });
103        }
104        Ok(())
105    }
106}
107
108/// Schema validation error.
109#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
110pub enum SchemaError {
111    #[error("unsupported schema version {found}, max supported is {max_supported}")]
112    UnsupportedVersion { found: u32, max_supported: u32 },
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn current_schema_validates() {
121        assert!(GraphSchema::current().validate().is_ok());
122    }
123
124    #[test]
125    fn future_schema_fails() {
126        let future = GraphSchema {
127            version: SCHEMA_VERSION + 1,
128        };
129        assert!(future.validate().is_err());
130    }
131
132    #[test]
133    fn node_type_roundtrip() {
134        let types = vec![
135            NodeType::Domain,
136            NodeType::Subdomain,
137            NodeType::Ip,
138            NodeType::Port,
139            NodeType::Service,
140            NodeType::Tech,
141            NodeType::Endpoint,
142            NodeType::Secret,
143            NodeType::Cloud,
144            NodeType::Finding,
145        ];
146        for t in types {
147            let s = serde_json::to_string(&t).unwrap();
148            let back: NodeType = serde_json::from_str(&s).unwrap();
149            assert_eq!(t, back);
150        }
151    }
152
153    #[test]
154    fn edge_type_roundtrip() {
155        let types = vec![
156            EdgeType::ResolvesTo,
157            EdgeType::Hosts,
158            EdgeType::Runs,
159            EdgeType::Exposes,
160            EdgeType::Leaks,
161            EdgeType::Misconfigured,
162            EdgeType::HasFinding,
163            EdgeType::HasService,
164        ];
165        for t in types {
166            let s = serde_json::to_string(&t).unwrap();
167            let back: EdgeType = serde_json::from_str(&s).unwrap();
168            assert_eq!(t, back);
169        }
170    }
171}