1use 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}