misp_client/
sightings.rs

1//! MISP sighting operations for tracking IOC observations.
2
3use serde_json::{json, Value};
4use tracing::debug;
5
6use crate::client::MispClient;
7use crate::error::MispError;
8use crate::models::Sighting;
9
10#[derive(Clone)]
11pub struct SightingsClient {
12    client: MispClient,
13}
14
15impl SightingsClient {
16    pub fn new(client: MispClient) -> Self {
17        Self { client }
18    }
19
20    pub async fn list_for_attribute(&self, attribute_id: &str) -> Result<Vec<Sighting>, MispError> {
21        debug!(%attribute_id, "Fetching sightings for attribute");
22        let resp = self
23            .client
24            .get(&format!(
25                "/sightings/listSightings/{}/attribute",
26                attribute_id
27            ))
28            .await?;
29        parse_sightings_list(resp)
30    }
31
32    pub async fn list_for_event(&self, event_id: &str) -> Result<Vec<Sighting>, MispError> {
33        debug!(%event_id, "Fetching sightings for event");
34        let resp = self
35            .client
36            .get(&format!("/sightings/listSightings/{}/event", event_id))
37            .await?;
38        parse_sightings_list(resp)
39    }
40
41    pub async fn search(&self, query: SightingSearchQuery) -> Result<Vec<Sighting>, MispError> {
42        debug!("Searching sightings");
43        let resp = self
44            .client
45            .post("/sightings/restSearch", Some(query.to_json()))
46            .await?;
47        parse_sightings_search(resp)
48    }
49
50    pub async fn search_by_value(&self, value: &str) -> Result<Vec<Sighting>, MispError> {
51        self.search(SightingSearchQuery::new().value(value)).await
52    }
53
54    pub async fn count_for_value(&self, value: &str) -> Result<SightingCount, MispError> {
55        let sightings = self.search_by_value(value).await?;
56
57        let mut positive = 0u64;
58        let mut negative = 0u64;
59        let mut expiration = 0u64;
60
61        for s in &sightings {
62            match s.sighting_type.as_deref() {
63                Some("0") | None => positive += 1,
64                Some("1") => negative += 1,
65                Some("2") => expiration += 1,
66                _ => positive += 1,
67            }
68        }
69
70        let first_seen = sightings
71            .iter()
72            .filter_map(|s| s.date_sighting.as_ref())
73            .min()
74            .cloned();
75
76        let last_seen = sightings
77            .iter()
78            .filter_map(|s| s.date_sighting.as_ref())
79            .max()
80            .cloned();
81
82        Ok(SightingCount {
83            total: sightings.len() as u64,
84            positive,
85            negative,
86            expiration,
87            first_seen,
88            last_seen,
89        })
90    }
91
92    pub async fn get_timeline(
93        &self,
94        value: &str,
95        limit: Option<u32>,
96    ) -> Result<Vec<SightingEntry>, MispError> {
97        let mut query = SightingSearchQuery::new().value(value);
98        if let Some(l) = limit {
99            query = query.limit(l);
100        }
101
102        let sightings = self.search(query).await?;
103
104        let mut entries: Vec<SightingEntry> = sightings
105            .into_iter()
106            .map(|s| SightingEntry {
107                id: s.id,
108                timestamp: s.date_sighting,
109                source: s.source,
110                org_name: s.organisation.map(|o| o.name),
111                sighting_type: match s.sighting_type.as_deref() {
112                    Some("0") | None => SightingType::Positive,
113                    Some("1") => SightingType::Negative,
114                    Some("2") => SightingType::Expiration,
115                    _ => SightingType::Positive,
116                },
117            })
118            .collect();
119
120        entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
121        Ok(entries)
122    }
123}
124
125impl std::fmt::Debug for SightingsClient {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("SightingsClient").finish()
128    }
129}
130
131#[derive(Debug, Default, Clone)]
132pub struct SightingSearchQuery {
133    pub value: Option<String>,
134    pub uuid: Option<String>,
135    pub attr_type: Option<String>,
136    pub source: Option<String>,
137    pub from: Option<String>,
138    pub to: Option<String>,
139    pub last: Option<String>,
140    pub limit: Option<u32>,
141    pub include_attribute: Option<bool>,
142    pub include_event: Option<bool>,
143}
144
145impl SightingSearchQuery {
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    pub fn value(mut self, v: impl Into<String>) -> Self {
151        self.value = Some(v.into());
152        self
153    }
154
155    pub fn uuid(mut self, u: impl Into<String>) -> Self {
156        self.uuid = Some(u.into());
157        self
158    }
159
160    pub fn attr_type(mut self, t: impl Into<String>) -> Self {
161        self.attr_type = Some(t.into());
162        self
163    }
164
165    pub fn source(mut self, s: impl Into<String>) -> Self {
166        self.source = Some(s.into());
167        self
168    }
169
170    pub fn from_date(mut self, from: impl Into<String>) -> Self {
171        self.from = Some(from.into());
172        self
173    }
174
175    pub fn to_date(mut self, to: impl Into<String>) -> Self {
176        self.to = Some(to.into());
177        self
178    }
179
180    pub fn last(mut self, duration: impl Into<String>) -> Self {
181        self.last = Some(duration.into());
182        self
183    }
184
185    pub fn limit(mut self, limit: u32) -> Self {
186        self.limit = Some(limit);
187        self
188    }
189
190    pub fn include_attribute(mut self) -> Self {
191        self.include_attribute = Some(true);
192        self
193    }
194
195    pub fn include_event(mut self) -> Self {
196        self.include_event = Some(true);
197        self
198    }
199
200    fn to_json(&self) -> Value {
201        let mut obj = serde_json::Map::new();
202        if let Some(ref v) = self.value {
203            obj.insert("value".into(), json!(v));
204        }
205        if let Some(ref v) = self.uuid {
206            obj.insert("uuid".into(), json!(v));
207        }
208        if let Some(ref v) = self.attr_type {
209            obj.insert("type".into(), json!(v));
210        }
211        if let Some(ref v) = self.source {
212            obj.insert("source".into(), json!(v));
213        }
214        if let Some(ref v) = self.from {
215            obj.insert("from".into(), json!(v));
216        }
217        if let Some(ref v) = self.to {
218            obj.insert("to".into(), json!(v));
219        }
220        if let Some(ref v) = self.last {
221            obj.insert("last".into(), json!(v));
222        }
223        if let Some(v) = self.limit {
224            obj.insert("limit".into(), json!(v));
225        }
226        if let Some(v) = self.include_attribute {
227            obj.insert("includeAttribute".into(), json!(v));
228        }
229        if let Some(v) = self.include_event {
230            obj.insert("includeEvent".into(), json!(v));
231        }
232        Value::Object(obj)
233    }
234}
235
236#[derive(Debug, Clone)]
237pub struct SightingCount {
238    pub total: u64,
239    pub positive: u64,
240    pub negative: u64,
241    pub expiration: u64,
242    pub first_seen: Option<String>,
243    pub last_seen: Option<String>,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum SightingType {
248    Positive,
249    Negative,
250    Expiration,
251}
252
253#[derive(Debug, Clone)]
254pub struct SightingEntry {
255    pub id: String,
256    pub timestamp: Option<String>,
257    pub source: Option<String>,
258    pub org_name: Option<String>,
259    pub sighting_type: SightingType,
260}
261
262fn parse_sightings_list(resp: Value) -> Result<Vec<Sighting>, MispError> {
263    if let Some(arr) = resp.as_array() {
264        let sightings: Result<Vec<Sighting>, _> = arr
265            .iter()
266            .filter_map(|v| v.get("Sighting"))
267            .map(|s| serde_json::from_value(s.clone()))
268            .collect();
269        return sightings.map_err(MispError::Parse);
270    }
271    if resp.is_object() && resp.get("Sighting").is_none() {
272        return Ok(Vec::new());
273    }
274    Err(MispError::InvalidResponse(
275        "unexpected sightings format".into(),
276    ))
277}
278
279fn parse_sightings_search(resp: Value) -> Result<Vec<Sighting>, MispError> {
280    if let Some(response) = resp.get("response") {
281        if let Some(arr) = response.as_array() {
282            let sightings: Result<Vec<Sighting>, _> = arr
283                .iter()
284                .filter_map(|v| v.get("Sighting"))
285                .map(|s| serde_json::from_value(s.clone()))
286                .collect();
287            return sightings.map_err(MispError::Parse);
288        }
289    }
290    parse_sightings_list(resp)
291}