1use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value, json};
9
10use crate::{
11 Error, Kibana, Result, Scope, SortOrder,
12 http::{Empty, Method},
13 pagination::paginated,
14 request::endpoint,
15 security::Severity,
16};
17
18pub const SECURITY_OWNER: &str = "securitySolution";
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
23#[non_exhaustive]
24pub struct Case {
25 pub id: String,
26 pub version: String,
28 pub title: String,
29 pub description: String,
30 pub owner: String,
31 pub status: String,
32 pub severity: String,
33 pub tags: Vec<String>,
34 pub created_at: String,
35 #[serde(default)]
36 pub updated_at: Option<String>,
37 #[serde(rename = "totalComment", default)]
38 pub total_comments: u64,
39 #[serde(flatten)]
40 pub extra: Map<String, Value>,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
44#[serde(rename_all = "kebab-case")]
45pub enum CaseStatus {
46 Open,
47 InProgress,
48 Closed,
49}
50
51impl CaseStatus {
52 fn as_str(self) -> &'static str {
53 match self {
54 Self::Open => "open",
55 Self::InProgress => "in-progress",
56 Self::Closed => "closed",
57 }
58 }
59}
60
61#[derive(Clone, Debug, Serialize)]
64pub struct NewCase {
65 title: String,
66 description: String,
67 owner: String,
68 severity: Severity,
69 tags: Vec<String>,
70 connector: Value,
71 settings: CaseSettings,
72}
73
74#[derive(Clone, Debug, Serialize)]
75struct CaseSettings {
76 #[serde(rename = "syncAlerts")]
77 sync_alerts: bool,
78}
79
80impl NewCase {
81 pub fn new(
82 title: impl Into<String>,
83 description: impl Into<String>,
84 owner: impl Into<String>,
85 ) -> Self {
86 Self {
87 title: title.into(),
88 description: description.into(),
89 owner: owner.into(),
90 severity: Severity::Low,
91 tags: Vec::new(),
92 connector: json!({"id": "none", "name": "none", "type": ".none", "fields": null}),
93 settings: CaseSettings { sync_alerts: false },
94 }
95 }
96
97 pub fn security(title: impl Into<String>, description: impl Into<String>) -> Self {
99 Self::new(title, description, SECURITY_OWNER)
100 }
101
102 pub fn severity(mut self, severity: Severity) -> Self {
103 self.severity = severity;
104 self
105 }
106
107 pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
108 self.tags = tags.into_iter().map(Into::into).collect();
109 self
110 }
111
112 pub fn sync_alerts(mut self, enabled: bool) -> Self {
114 self.settings.sync_alerts = enabled;
115 self
116 }
117
118 pub fn connector(mut self, connector: Value) -> Self {
120 self.connector = connector;
121 self
122 }
123}
124
125#[derive(Clone, Debug, Serialize)]
128pub struct CasePatch {
129 id: String,
130 version: String,
131 #[serde(flatten)]
132 changes: Map<String, Value>,
133}
134
135impl CasePatch {
136 pub fn new(id: impl Into<String>, version: impl Into<String>) -> Self {
138 Self {
139 id: id.into(),
140 version: version.into(),
141 changes: Map::new(),
142 }
143 }
144
145 pub fn status(self, status: CaseStatus) -> Self {
146 self.set("status", status.as_str().into())
147 }
148
149 pub fn title(self, title: impl Into<String>) -> Self {
150 self.set("title", Value::String(title.into()))
151 }
152
153 pub fn description(self, description: impl Into<String>) -> Self {
154 self.set("description", Value::String(description.into()))
155 }
156
157 pub fn severity(self, severity: Severity) -> Self {
158 self.set("severity", severity.as_str().into())
159 }
160
161 pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(self, tags: I) -> Self {
162 self.set(
163 "tags",
164 tags.into_iter().map(|t| Value::String(t.into())).collect(),
165 )
166 }
167
168 pub fn field(self, name: &str, value: impl Serialize) -> Result<Self> {
184 if matches!(name, "id" | "version") {
185 return Err(Error::InvalidRequest(format!(
186 "case patch field {name:?} is reserved"
187 )));
188 }
189 Ok(self.set(name, serde_json::to_value(value).map_err(Error::serialize)?))
190 }
191
192 fn set(mut self, key: &str, value: Value) -> Self {
193 self.changes.insert(key.into(), value);
194 self
195 }
196}
197
198#[derive(Clone, Debug, Serialize)]
200pub struct CaseComment {
201 #[serde(rename = "type")]
202 kind: &'static str,
203 owner: String,
204 comment: String,
205}
206
207impl CaseComment {
208 pub fn user(owner: impl Into<String>, comment: impl Into<String>) -> Self {
210 Self {
211 kind: "user",
212 owner: owner.into(),
213 comment: comment.into(),
214 }
215 }
216}
217
218#[derive(Clone, Debug, Serialize, Deserialize)]
219#[non_exhaustive]
220pub struct CasePage {
221 pub cases: Vec<Case>,
222 pub page: u32,
223 pub per_page: u32,
224 pub total: u64,
225 #[serde(flatten)]
227 pub extra: Map<String, Value>,
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
231#[non_exhaustive]
232pub struct CommentPage {
233 pub comments: Vec<Value>,
235 pub page: u32,
236 pub per_page: u32,
237 pub total: u64,
238}
239
240#[derive(Clone, Copy, Debug)]
241pub struct Cases<'a>(pub(crate) &'a Kibana);
242
243impl<'a> Cases<'a> {
244 pub fn find(&self) -> FindCases<'a> {
246 FindCases(
247 self.0
248 .request(Method::GET, Scope::Space, &["api", "cases", "_find"]),
249 )
250 }
251
252 pub fn get(&self, id: &str) -> GetCase<'a> {
253 GetCase(
254 self.0
255 .request(Method::GET, Scope::Space, &["api", "cases", id]),
256 )
257 }
258
259 pub fn create<B: Serialize + ?Sized>(&self, case: &B) -> CreateCase<'a> {
261 CreateCase(
262 self.0
263 .request(Method::POST, Scope::Space, &["api", "cases"])
264 .json(case),
265 )
266 }
267
268 pub fn update(&self, patches: impl IntoIterator<Item = CasePatch>) -> UpdateCases<'a> {
270 let cases: Vec<CasePatch> = patches.into_iter().collect();
271 UpdateCases(
272 self.0
273 .request(Method::PATCH, Scope::Space, &["api", "cases"])
274 .field("cases", cases),
275 )
276 }
277
278 pub fn delete<I: IntoIterator<Item = S>, S: AsRef<str>>(&self, ids: I) -> DeleteCases<'a> {
280 let ids: Vec<String> = ids.into_iter().map(|id| id.as_ref().to_owned()).collect();
281 DeleteCases(
282 self.0
283 .request(Method::DELETE, Scope::Space, &["api", "cases"])
284 .param("ids", Value::from(ids)),
285 )
286 }
287
288 pub fn add_comment<B: Serialize + ?Sized>(&self, case_id: &str, comment: &B) -> AddComment<'a> {
290 AddComment(
291 self.0
292 .request(
293 Method::POST,
294 Scope::Space,
295 &["api", "cases", case_id, "comments"],
296 )
297 .json(comment),
298 )
299 }
300
301 pub fn find_comments(&self, case_id: &str) -> FindComments<'a> {
302 FindComments(self.0.request(
303 Method::GET,
304 Scope::Space,
305 &["api", "cases", case_id, "comments", "_find"],
306 ))
307 }
308}
309
310endpoint! {
311 FindCases => CasePage
313}
314
315impl FindCases<'_> {
316 pub fn page(self, page: u32) -> Self {
318 Self(self.0.positive_param("page", page))
319 }
320
321 pub fn per_page(self, per_page: u32) -> Self {
322 Self(self.0.positive_param("perPage", per_page))
323 }
324
325 pub fn owner(self, owner: &str) -> Self {
327 Self(self.0.append_param("owner", owner))
328 }
329
330 pub fn search(self, text: &str) -> Self {
331 Self(self.0.param("search", text))
332 }
333
334 pub fn status(self, status: CaseStatus) -> Self {
335 Self(self.0.param("status", status.as_str()))
336 }
337
338 pub fn severity(self, severity: Severity) -> Self {
339 Self(self.0.param("severity", severity.as_str()))
340 }
341
342 pub fn tag(self, tag: &str) -> Self {
344 Self(self.0.append_param("tags", tag))
345 }
346
347 pub fn sort_field(self, field: &str) -> Self {
349 Self(self.0.param("sortField", field))
350 }
351
352 pub fn sort_order(self, order: SortOrder) -> Self {
353 Self(self.0.param("sortOrder", order.as_str()))
354 }
355}
356
357endpoint! {
358 GetCase => Case
360}
361
362endpoint! {
363 CreateCase => Case
365}
366
367endpoint! {
368 UpdateCases => Vec<Case>
370}
371
372endpoint! {
373 DeleteCases => Empty
375}
376
377endpoint! {
378 AddComment => Case
380}
381
382endpoint! {
383 FindComments => CommentPage
385}
386
387impl FindComments<'_> {
388 pub fn page(self, page: u32) -> Self {
390 Self(self.0.positive_param("page", page))
391 }
392
393 pub fn per_page(self, per_page: u32) -> Self {
394 Self(self.0.positive_param("perPage", per_page))
395 }
396
397 pub fn sort_order(self, order: SortOrder) -> Self {
398 Self(self.0.param("sortOrder", order.as_str()))
399 }
400}
401
402paginated!(FindCases => CasePage, FindComments => CommentPage);