1use serde::{Deserialize, Serialize};
7
8use wist_contracts::API_VERSION_V1;
9
10pub const API_VERSION: &str = API_VERSION_V1;
12
13pub const REPORT_AGENT_FACT_SUMMARY_KIND: &str = "report_agent_fact_summary";
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum FactSummaryAckStatus {
19 Accepted,
21 Duplicate,
23 Rejected,
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40#[serde(deny_unknown_fields)]
41pub struct ReportAgentFactSummary {
42 pub api_version: String,
43 pub kind: String,
44 pub report_id: String,
45 pub agent_id: String,
46 pub instance_id: String,
47 pub content_digest: String,
49 pub revision: i64,
51 pub observed_at: String,
53 pub os: String,
54 pub arch: String,
55 pub process_count: i64,
57 pub process_executables: Vec<String>,
60 pub packages: Vec<String>,
62 pub listen_ports: Vec<String>,
63 #[serde(default)]
71 pub host_id: String,
72 #[serde(default)]
74 pub host_name: String,
75 #[serde(default)]
77 pub network_addresses: Vec<String>,
78 pub reported_at: String,
79}
80
81impl ReportAgentFactSummary {
82 #[allow(clippy::too_many_arguments)]
83 pub fn new_agent_facts(
84 report_id: String,
85 agent_id: String,
86 instance_id: String,
87 content_digest: String,
88 revision: i64,
89 observed_at: String,
90 os: String,
91 arch: String,
92 process_count: i64,
93 process_executables: Vec<String>,
94 packages: Vec<String>,
95 listen_ports: Vec<String>,
96 reported_at: String,
97 ) -> Self {
98 Self {
99 api_version: API_VERSION_V1.to_string(),
100 kind: REPORT_AGENT_FACT_SUMMARY_KIND.to_string(),
101 report_id,
102 agent_id,
103 instance_id,
104 content_digest,
105 revision,
106 observed_at,
107 os,
108 arch,
109 process_count,
110 process_executables,
111 packages,
112 listen_ports,
113 host_id: String::new(),
114 host_name: String::new(),
115 network_addresses: Vec::new(),
116 reported_at,
117 }
118 }
119
120 pub fn with_display(
126 mut self,
127 host_id: String,
128 host_name: String,
129 network_addresses: Vec<String>,
130 ) -> Self {
131 self.host_id = host_id;
132 self.host_name = host_name;
133 self.network_addresses = network_addresses;
134 self
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(deny_unknown_fields)]
141pub struct FactSummaryAccepted {
142 pub report_id: String,
143 pub agent_id: String,
144 pub content_digest: String,
145 pub ack_status: FactSummaryAckStatus,
146 pub suggestion_id: Option<String>,
148 pub received_at: String,
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn fact_summary_ack_status_uses_snake_case_and_rejects_unknown() {
157 assert_eq!(
158 serde_json::to_string(&FactSummaryAckStatus::Duplicate).unwrap(),
159 "\"duplicate\""
160 );
161 assert!(serde_json::from_str::<FactSummaryAckStatus>("\"nope\"").is_err());
162 }
163
164 #[test]
165 fn new_agent_facts_defaults_display_fields_then_with_display_fills_them() {
166 let summary = ReportAgentFactSummary::new_agent_facts(
167 "fact_1".to_string(),
168 "agent-1".to_string(),
169 "inst-1".to_string(),
170 "fact-v1:sha256:abc".to_string(),
171 7,
172 "2026-09-27T00:00:00Z".to_string(),
173 "macos".to_string(),
174 "arm64".to_string(),
175 3,
176 vec!["/usr/bin/a".to_string()],
177 Vec::new(),
178 vec!["443".to_string()],
179 "2026-09-27T00:00:01Z".to_string(),
180 );
181 assert_eq!(summary.kind, REPORT_AGENT_FACT_SUMMARY_KIND);
182 assert!(summary.host_id.is_empty());
183 assert!(summary.network_addresses.is_empty());
184
185 let with_display = summary.with_display(
186 "host-id".to_string(),
187 "host-name".to_string(),
188 vec!["en0 10.0.0.1/24".to_string()],
189 );
190 assert_eq!(with_display.host_id, "host-id");
191 assert_eq!(with_display.host_name, "host-name");
192
193 let json = serde_json::to_string(&with_display).expect("encode");
194 let back: ReportAgentFactSummary = serde_json::from_str(&json).expect("decode");
195 assert_eq!(back, with_display);
196 }
197}