Skip to main content

gossan_graph/
node.rs

1//! Typed graph node.
2
3use serde::{Deserialize, Serialize};
4
5use crate::schema::NodeType;
6
7/// A node in the attack-surface graph.
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct Node {
10    /// Stable unique identifier.
11    pub id: String,
12    /// Semantic type.
13    pub kind: NodeType,
14    /// Human-readable label.
15    pub label: String,
16    /// Optional JSON payload with type-specific fields.
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub payload: Option<serde_json::Value>,
19    /// Unix timestamp (ms) when the node was first observed.
20    #[serde(default)]
21    pub first_seen_ms: u64,
22    /// Unix timestamp (ms) when the node was last observed.
23    #[serde(default)]
24    pub last_seen_ms: u64,
25}
26
27impl Node {
28    /// Create a new node with the current time as `first_seen`.
29    #[must_use]
30    pub fn new(id: impl Into<String>, kind: NodeType, label: impl Into<String>) -> Self {
31        let now = std::time::SystemTime::now()
32            .duration_since(std::time::UNIX_EPOCH)
33            .unwrap_or_default()
34            .as_millis() as u64;
35        Self {
36            id: id.into(),
37            kind,
38            label: label.into(),
39            payload: None,
40            first_seen_ms: now,
41            last_seen_ms: now,
42        }
43    }
44
45    /// Attach a JSON payload.
46    #[must_use]
47    pub fn with_payload(mut self, payload: impl Serialize) -> Self {
48        self.payload = serde_json::to_value(payload).ok();
49        self
50    }
51}