1use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11use serde_json::{Map, Value};
12
13use crate::{
14 Error, ImportFailure, Kibana, Result, Scope, SortOrder,
15 exceptions::ListReference,
16 http::{Method, Raw},
17 pagination::paginated,
18 request::endpoint,
19};
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "lowercase")]
23pub enum Severity {
24 Low,
25 Medium,
26 High,
27 Critical,
28}
29
30impl Severity {
31 pub(crate) fn as_str(self) -> &'static str {
32 match self {
33 Self::Low => "low",
34 Self::Medium => "medium",
35 Self::High => "high",
36 Self::Critical => "critical",
37 }
38 }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
43#[serde(transparent)]
44pub struct RiskScore(u8);
45
46impl RiskScore {
47 pub fn new(score: u8) -> Result<Self> {
49 if score > 100 {
50 return Err(Error::InvalidRequest(format!(
51 "risk score {score} is above 100"
52 )));
53 }
54 Ok(Self(score))
55 }
56
57 pub fn get(self) -> u8 {
58 self.0
59 }
60}
61
62impl TryFrom<u8> for RiskScore {
63 type Error = Error;
64
65 fn try_from(score: u8) -> Result<Self> {
66 Self::new(score)
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
78pub struct RuleSchedule {
79 interval: u64,
80 additional_lookback: u64,
81}
82
83impl RuleSchedule {
84 pub fn new(interval: Duration, additional_lookback: Duration) -> Result<Self> {
87 if interval.subsec_nanos() != 0 || additional_lookback.subsec_nanos() != 0 {
88 return Err(Error::InvalidRequest(
89 "rule schedules use whole seconds".into(),
90 ));
91 }
92 if interval.is_zero() {
93 return Err(Error::InvalidRequest(
94 "a rule interval must be at least one second".into(),
95 ));
96 }
97 Ok(Self {
98 interval: interval.as_secs(),
99 additional_lookback: additional_lookback.as_secs(),
100 })
101 }
102
103 pub fn every(interval: Duration) -> Result<Self> {
105 Self::new(interval, Duration::from_secs(60))
106 }
107
108 pub fn interval(self) -> Duration {
109 Duration::from_secs(self.interval)
110 }
111
112 pub fn lookback(self) -> Duration {
114 Duration::from_secs(self.interval.saturating_add(self.additional_lookback))
115 }
116
117 fn interval_value(self) -> String {
118 date_math(self.interval)
119 }
120
121 fn lookback_value(self) -> String {
122 format!("now-{}", date_math(self.lookback().as_secs()))
123 }
124}
125
126impl Default for RuleSchedule {
127 fn default() -> Self {
129 Self {
130 interval: 300,
131 additional_lookback: 60,
132 }
133 }
134}
135
136fn date_math(seconds: u64) -> String {
138 match seconds {
139 s if s % 3600 == 0 => format!("{}h", s / 3600),
140 s if s % 60 == 0 => format!("{}m", s / 60),
141 s => format!("{s}s"),
142 }
143}
144
145#[derive(Clone, Debug, Serialize, Deserialize)]
147#[non_exhaustive]
148pub struct DetectionRule {
149 pub id: String,
151 pub rule_id: String,
153 pub name: String,
154 pub description: String,
155 pub enabled: bool,
156 pub severity: String,
157 pub risk_score: f64,
158 #[serde(rename = "type")]
159 pub rule_type: String,
160 #[serde(default)]
161 pub query: Option<String>,
162 #[serde(default)]
163 pub tags: Vec<String>,
164 #[serde(default)]
165 pub updated_at: Option<String>,
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub execution_summary: Option<RuleExecutionSummary>,
168 #[serde(flatten)]
170 pub extra: Map<String, Value>,
171}
172
173#[derive(Clone, Debug, Serialize, Deserialize)]
174#[non_exhaustive]
175pub struct RuleExecutionSummary {
176 pub last_execution: RuleExecution,
177 #[serde(flatten)]
178 pub extra: Map<String, Value>,
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize)]
183#[non_exhaustive]
184pub struct RuleExecution {
185 pub status: String,
186 pub date: String,
187 pub message: String,
188 #[serde(flatten)]
189 pub extra: Map<String, Value>,
190}
191
192#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
193#[serde(rename_all = "lowercase")]
194pub enum QueryLanguage {
195 Kuery,
196 Lucene,
197}
198
199#[derive(Clone, Debug, Serialize)]
204pub struct QueryRule {
205 #[serde(rename = "type")]
206 rule_type: &'static str,
207 name: String,
208 description: String,
209 query: String,
210 language: QueryLanguage,
211 index: Vec<String>,
212 severity: Severity,
213 risk_score: RiskScore,
214 enabled: bool,
215 interval: String,
216 from: String,
217 tags: Vec<String>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 rule_id: Option<String>,
220 #[serde(skip_serializing_if = "Vec::is_empty")]
221 exceptions_list: Vec<ListReference>,
222}
223
224impl QueryRule {
225 pub fn new(
242 name: impl Into<String>,
243 description: impl Into<String>,
244 query: impl Into<String>,
245 ) -> Self {
246 Self {
247 rule_type: "query",
248 name: name.into(),
249 description: description.into(),
250 query: query.into(),
251 language: QueryLanguage::Kuery,
252 index: vec!["logs-*".into()],
253 severity: Severity::Medium,
254 risk_score: RiskScore(47),
255 enabled: false,
256 interval: RuleSchedule::default().interval_value(),
257 from: RuleSchedule::default().lookback_value(),
258 tags: Vec::new(),
259 rule_id: None,
260 exceptions_list: Vec::new(),
261 }
262 }
263
264 pub fn language(mut self, language: QueryLanguage) -> Self {
265 self.language = language;
266 self
267 }
268
269 pub fn index<I: IntoIterator<Item = S>, S: Into<String>>(mut self, patterns: I) -> Self {
270 self.index = patterns.into_iter().map(Into::into).collect();
271 self
272 }
273
274 pub fn severity(mut self, severity: Severity) -> Self {
275 self.severity = severity;
276 self
277 }
278
279 pub fn risk_score(mut self, score: RiskScore) -> Self {
280 self.risk_score = score;
281 self
282 }
283
284 pub fn enabled(mut self, enabled: bool) -> Self {
285 self.enabled = enabled;
286 self
287 }
288
289 pub fn schedule(mut self, schedule: RuleSchedule) -> Self {
291 self.interval = schedule.interval_value();
292 self.from = schedule.lookback_value();
293 self
294 }
295
296 pub fn custom_schedule(mut self, interval: impl Into<String>, from: impl Into<String>) -> Self {
300 self.interval = interval.into();
301 self.from = from.into();
302 self
303 }
304
305 pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
306 self.tags = tags.into_iter().map(Into::into).collect();
307 self
308 }
309
310 pub fn rule_id(mut self, rule_id: impl Into<String>) -> Self {
312 self.rule_id = Some(rule_id.into());
313 self
314 }
315
316 pub fn exceptions_list(mut self, lists: Vec<ListReference>) -> Self {
317 self.exceptions_list = lists;
318 self
319 }
320}
321
322#[derive(Clone, Copy, Debug)]
324pub enum RuleSelector<'a> {
325 Id(&'a str),
326 RuleId(&'a str),
327}
328
329impl<'a> RuleSelector<'a> {
330 fn pair(self) -> (&'static str, &'a str) {
331 match self {
332 Self::Id(v) => ("id", v),
333 Self::RuleId(v) => ("rule_id", v),
334 }
335 }
336}
337
338#[derive(Clone, Debug, Deserialize, Serialize)]
339#[non_exhaustive]
340pub struct RulePage {
341 pub data: Vec<DetectionRule>,
342 pub page: u32,
343 #[serde(rename = "perPage", alias = "per_page")]
344 pub per_page: u32,
345 pub total: u64,
346}
347
348#[derive(Clone, Debug, Deserialize, Serialize)]
351#[non_exhaustive]
352pub struct RuleImportResult {
353 pub success: bool,
354 pub success_count: u64,
355 #[serde(default)]
356 pub errors: Vec<ImportFailure>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
358 pub exceptions_success: Option<bool>,
359 #[serde(default)]
360 pub exceptions_errors: Vec<ImportFailure>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub action_connectors_success: Option<bool>,
363 #[serde(default)]
364 pub action_connectors_errors: Vec<ImportFailure>,
365 #[serde(flatten)]
366 pub extra: Map<String, Value>,
367}
368
369#[derive(Clone, Copy, Debug)]
370pub struct Security<'a>(pub(crate) &'a Kibana);
371
372impl<'a> Security<'a> {
373 pub fn privileges(&self) -> GetPrivileges<'a> {
375 GetPrivileges(self.0.request(
376 Method::GET,
377 Scope::Space,
378 &["api", "detection_engine", "privileges"],
379 ))
380 }
381
382 pub fn create_alerts_index(&self) -> CreateAlertsIndex<'a> {
384 CreateAlertsIndex(self.0.request(
385 Method::POST,
386 Scope::Space,
387 &["api", "detection_engine", "index"],
388 ))
389 }
390
391 pub fn find_rules(&self) -> FindRules<'a> {
393 FindRules(self.0.request(
394 Method::GET,
395 Scope::Space,
396 &["api", "detection_engine", "rules", "_find"],
397 ))
398 }
399
400 pub fn get_rule(&self, rule: RuleSelector<'_>) -> GetRule<'a> {
401 GetRule(
402 self.0
403 .request(
404 Method::GET,
405 Scope::Space,
406 &["api", "detection_engine", "rules"],
407 )
408 .selector(rule.pair()),
409 )
410 }
411
412 pub fn create_rule<B: Serialize + ?Sized>(&self, rule: &B) -> CreateRule<'a> {
414 CreateRule(
415 self.0
416 .request(
417 Method::POST,
418 Scope::Space,
419 &["api", "detection_engine", "rules"],
420 )
421 .json(rule),
422 )
423 }
424
425 pub fn patch_rule(&self, rule: RuleSelector<'_>) -> PatchRule<'a> {
429 let (key, value) = rule.pair();
430 PatchRule(
431 self.0
432 .request(
433 Method::PATCH,
434 Scope::Space,
435 &["api", "detection_engine", "rules"],
436 )
437 .nonempty(key, value)
438 .field(key, value),
439 )
440 }
441
442 pub fn delete_rule(&self, rule: RuleSelector<'_>) -> DeleteRule<'a> {
443 DeleteRule(
444 self.0
445 .request(
446 Method::DELETE,
447 Scope::Space,
448 &["api", "detection_engine", "rules"],
449 )
450 .selector(rule.pair()),
451 )
452 }
453
454 pub fn export_rules(&self) -> ExportRules<'a> {
456 ExportRules(self.0.request(
457 Method::POST,
458 Scope::Space,
459 &["api", "detection_engine", "rules", "_export"],
460 ))
461 }
462
463 pub fn import_rules(&self, ndjson: impl Into<Vec<u8>>) -> ImportRules<'a> {
465 ImportRules(
466 self.0
467 .request(
468 Method::POST,
469 Scope::Space,
470 &["api", "detection_engine", "rules", "_import"],
471 )
472 .file("rules.ndjson", "application/x-ndjson", ndjson.into()),
473 )
474 }
475}
476
477endpoint! {
478 GetPrivileges => Value
480}
481
482endpoint! {
483 CreateAlertsIndex => Value
485}
486
487endpoint! {
488 FindRules => RulePage
490}
491
492impl FindRules<'_> {
493 pub fn page(self, page: u32) -> Self {
495 Self(self.0.positive_param("page", page))
496 }
497
498 pub fn per_page(self, per_page: u32) -> Self {
499 Self(self.0.positive_param("per_page", per_page))
500 }
501
502 pub fn filter(self, filter: &str) -> Self {
504 Self(self.0.param("filter", filter))
505 }
506
507 pub fn sort_field(self, field: &str) -> Self {
508 Self(self.0.param("sort_field", field))
509 }
510
511 pub fn sort_order(self, order: SortOrder) -> Self {
512 Self(self.0.param("sort_order", order.as_str()))
513 }
514}
515
516endpoint! {
517 GetRule => DetectionRule
519}
520
521endpoint! {
522 CreateRule => DetectionRule
524}
525
526endpoint! {
527 PatchRule => DetectionRule
529}
530
531const RESERVED_PATCH_FIELDS: &[&str] = &[
533 "id",
534 "rule_id",
535 "risk_score",
536 "severity",
537 "interval",
538 "from",
539];
540
541impl PatchRule<'_> {
542 pub fn name(self, name: &str) -> Self {
543 Self(self.0.field("name", name))
544 }
545
546 pub fn description(self, description: &str) -> Self {
547 Self(self.0.field("description", description))
548 }
549
550 pub fn enabled(self, enabled: bool) -> Self {
551 Self(self.0.field("enabled", enabled))
552 }
553
554 pub fn query(self, query: &str) -> Self {
555 Self(self.0.field("query", query))
556 }
557
558 pub fn severity(self, severity: Severity) -> Self {
559 Self(self.0.field("severity", severity))
560 }
561
562 pub fn risk_score(self, score: RiskScore) -> Self {
563 Self(self.0.field("risk_score", score))
564 }
565
566 pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(self, tags: I) -> Self {
567 let tags: Vec<String> = tags.into_iter().map(Into::into).collect();
568 Self(self.0.field("tags", tags))
569 }
570
571 pub fn exceptions_list(self, lists: Vec<ListReference>) -> Self {
573 Self(self.0.field("exceptions_list", lists))
574 }
575
576 pub fn schedule(self, schedule: RuleSchedule) -> Self {
578 Self(
579 self.0
580 .field("interval", schedule.interval_value())
581 .field("from", schedule.lookback_value()),
582 )
583 }
584
585 pub fn field(self, name: &str, value: impl Serialize) -> Self {
605 if RESERVED_PATCH_FIELDS.contains(&name) {
606 return Self(self.0.invalid(format!(
607 "{name:?} is set by the selector or a typed setter; use unchecked_field to override it"
608 )));
609 }
610 Self(self.0.field(name, value))
611 }
612
613 pub fn unchecked_field(self, name: &str, value: impl Serialize) -> Self {
616 Self(self.0.field(name, value))
617 }
618}
619
620endpoint! {
621 DeleteRule => DetectionRule
623}
624
625endpoint! {
626 ExportRules => Raw
628}
629
630impl ExportRules<'_> {
631 pub fn rule_ids<I: IntoIterator<Item = S>, S: AsRef<str>>(self, rule_ids: I) -> Self {
633 let objects: Vec<Value> = rule_ids
634 .into_iter()
635 .map(|id| serde_json::json!({ "rule_id": id.as_ref() }))
636 .collect();
637 Self(self.0.field("objects", objects))
638 }
639
640 pub fn exclude_export_details(self, exclude: bool) -> Self {
642 Self(self.0.param("exclude_export_details", exclude))
643 }
644
645 pub fn file_name(self, name: &str) -> Self {
646 Self(self.0.param("file_name", name))
647 }
648}
649
650endpoint! {
651 ImportRules => RuleImportResult
653}
654
655impl ImportRules<'_> {
656 pub fn overwrite(self, overwrite: bool) -> Self {
658 Self(self.0.param("overwrite", overwrite))
659 }
660
661 pub fn overwrite_exceptions(self, overwrite: bool) -> Self {
662 Self(self.0.param("overwrite_exceptions", overwrite))
663 }
664
665 pub fn overwrite_action_connectors(self, overwrite: bool) -> Self {
666 Self(self.0.param("overwrite_action_connectors", overwrite))
667 }
668
669 pub fn as_new_list(self, enabled: bool) -> Self {
671 Self(self.0.param("as_new_list", enabled))
672 }
673}
674
675paginated!(FindRules => RulePage);