misp_client/
galaxies.rs

1//! MISP galaxy and cluster operations (threat actors, MITRE ATT&CK, malware).
2
3use serde_json::{json, Value};
4use tracing::debug;
5
6use crate::client::MispClient;
7use crate::error::MispError;
8use crate::models::{Galaxy, GalaxyCluster};
9
10#[derive(Clone)]
11pub struct GalaxiesClient {
12    client: MispClient,
13}
14
15impl GalaxiesClient {
16    pub fn new(client: MispClient) -> Self {
17        Self { client }
18    }
19
20    pub async fn list(&self) -> Result<Vec<Galaxy>, MispError> {
21        debug!("Listing galaxies");
22        let resp = self.client.get("/galaxies").await?;
23        parse_galaxies_list(resp)
24    }
25
26    pub async fn get(&self, id: &str) -> Result<Galaxy, MispError> {
27        debug!(%id, "Fetching galaxy");
28        let resp = self.client.get(&format!("/galaxies/view/{}", id)).await?;
29        parse_galaxy_response(resp)
30    }
31
32    pub async fn get_cluster(&self, cluster_id: &str) -> Result<GalaxyCluster, MispError> {
33        debug!(%cluster_id, "Fetching galaxy cluster");
34        let resp = self
35            .client
36            .get(&format!("/galaxy_clusters/view/{}", cluster_id))
37            .await?;
38        parse_cluster_response(resp)
39    }
40
41    pub async fn search_clusters(
42        &self,
43        query: ClusterSearchQuery,
44    ) -> Result<Vec<GalaxyCluster>, MispError> {
45        debug!("Searching galaxy clusters");
46        let resp = self
47            .client
48            .post("/galaxy_clusters/restSearch", Some(query.to_json()))
49            .await?;
50        parse_clusters_search(resp)
51    }
52
53    pub async fn search_clusters_by_value(
54        &self,
55        value: &str,
56    ) -> Result<Vec<GalaxyCluster>, MispError> {
57        self.search_clusters(ClusterSearchQuery::new().value(value))
58            .await
59    }
60
61    pub async fn get_by_type(&self, galaxy_type: &str) -> Result<Option<Galaxy>, MispError> {
62        let galaxies = self.list().await?;
63        Ok(galaxies.into_iter().find(|g| g.galaxy_type == galaxy_type))
64    }
65
66    pub async fn get_mitre_attack(&self) -> Result<Option<Galaxy>, MispError> {
67        self.get_by_type("mitre-attack-pattern").await
68    }
69
70    pub async fn get_threat_actors(&self) -> Result<Option<Galaxy>, MispError> {
71        self.get_by_type("threat-actor").await
72    }
73
74    pub async fn get_malware(&self) -> Result<Option<Galaxy>, MispError> {
75        self.get_by_type("malpedia").await
76    }
77
78    pub async fn search_threat_actor(&self, name: &str) -> Result<Vec<GalaxyCluster>, MispError> {
79        self.search_clusters(
80            ClusterSearchQuery::new()
81                .value(name)
82                .galaxy_type("threat-actor"),
83        )
84        .await
85    }
86
87    pub async fn search_attack_pattern(&self, name: &str) -> Result<Vec<GalaxyCluster>, MispError> {
88        self.search_clusters(
89            ClusterSearchQuery::new()
90                .value(name)
91                .galaxy_type("mitre-attack-pattern"),
92        )
93        .await
94    }
95}
96
97impl std::fmt::Debug for GalaxiesClient {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        f.debug_struct("GalaxiesClient").finish()
100    }
101}
102
103#[derive(Debug, Default, Clone)]
104pub struct ClusterSearchQuery {
105    pub value: Option<String>,
106    pub galaxy_type: Option<String>,
107    pub tag_name: Option<String>,
108    pub uuid: Option<String>,
109    pub source: Option<String>,
110}
111
112impl ClusterSearchQuery {
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    pub fn value(mut self, v: impl Into<String>) -> Self {
118        self.value = Some(v.into());
119        self
120    }
121
122    pub fn galaxy_type(mut self, t: impl Into<String>) -> Self {
123        self.galaxy_type = Some(t.into());
124        self
125    }
126
127    pub fn tag_name(mut self, t: impl Into<String>) -> Self {
128        self.tag_name = Some(t.into());
129        self
130    }
131
132    pub fn uuid(mut self, u: impl Into<String>) -> Self {
133        self.uuid = Some(u.into());
134        self
135    }
136
137    pub fn source(mut self, s: impl Into<String>) -> Self {
138        self.source = Some(s.into());
139        self
140    }
141
142    fn to_json(&self) -> Value {
143        let mut obj = serde_json::Map::new();
144        if let Some(ref v) = self.value {
145            obj.insert("value".into(), json!(v));
146        }
147        if let Some(ref v) = self.galaxy_type {
148            obj.insert("type".into(), json!(v));
149        }
150        if let Some(ref v) = self.tag_name {
151            obj.insert("tag_name".into(), json!(v));
152        }
153        if let Some(ref v) = self.uuid {
154            obj.insert("uuid".into(), json!(v));
155        }
156        if let Some(ref v) = self.source {
157            obj.insert("source".into(), json!(v));
158        }
159        Value::Object(obj)
160    }
161}
162
163fn parse_galaxies_list(resp: Value) -> Result<Vec<Galaxy>, MispError> {
164    if let Some(arr) = resp.as_array() {
165        let galaxies: Result<Vec<Galaxy>, _> = arr
166            .iter()
167            .filter_map(|v| v.get("Galaxy"))
168            .map(|g| serde_json::from_value(g.clone()))
169            .collect();
170        return galaxies.map_err(MispError::Parse);
171    }
172    Err(MispError::InvalidResponse(
173        "expected array of galaxies".into(),
174    ))
175}
176
177fn parse_galaxy_response(resp: Value) -> Result<Galaxy, MispError> {
178    if let Some(galaxy) = resp.get("Galaxy") {
179        let mut g: Galaxy = serde_json::from_value(galaxy.clone()).map_err(MispError::Parse)?;
180        if let Some(clusters) = resp.get("GalaxyCluster").and_then(|v| v.as_array()) {
181            g.clusters = clusters
182                .iter()
183                .filter_map(|c| serde_json::from_value(c.clone()).ok())
184                .collect();
185        }
186        return Ok(g);
187    }
188    Err(MispError::InvalidResponse("missing Galaxy wrapper".into()))
189}
190
191fn parse_cluster_response(resp: Value) -> Result<GalaxyCluster, MispError> {
192    if let Some(cluster) = resp.get("GalaxyCluster") {
193        return serde_json::from_value(cluster.clone()).map_err(MispError::Parse);
194    }
195    Err(MispError::InvalidResponse(
196        "missing GalaxyCluster wrapper".into(),
197    ))
198}
199
200fn parse_clusters_search(resp: Value) -> Result<Vec<GalaxyCluster>, MispError> {
201    if let Some(response) = resp.get("response") {
202        if let Some(arr) = response.as_array() {
203            let clusters: Result<Vec<GalaxyCluster>, _> = arr
204                .iter()
205                .filter_map(|v| v.get("GalaxyCluster"))
206                .map(|c| serde_json::from_value(c.clone()))
207                .collect();
208            return clusters.map_err(MispError::Parse);
209        }
210    }
211    if let Some(arr) = resp.as_array() {
212        let clusters: Result<Vec<GalaxyCluster>, _> = arr
213            .iter()
214            .filter_map(|v| v.get("GalaxyCluster"))
215            .map(|c| serde_json::from_value(c.clone()))
216            .collect();
217        return clusters.map_err(MispError::Parse);
218    }
219    Err(MispError::InvalidResponse(
220        "unexpected cluster search format".into(),
221    ))
222}