use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::{
Kibana, Request, Scope, SortOrder,
http::{Method, Raw},
pagination::paginated,
request::endpoint,
};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NamespaceType {
#[default]
Single,
Agnostic,
}
impl NamespaceType {
fn as_str(self) -> &'static str {
match self {
Self::Single => "single",
Self::Agnostic => "agnostic",
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum OsType {
Linux,
Macos,
Windows,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ListReference {
pub id: String,
pub list_id: String,
pub namespace_type: NamespaceType,
#[serde(rename = "type")]
pub list_type: String,
}
impl ListReference {
pub fn new(
id: impl Into<String>,
list_id: impl Into<String>,
namespace_type: NamespaceType,
list_type: impl Into<String>,
) -> Self {
Self {
id: id.into(),
list_id: list_id.into(),
namespace_type,
list_type: list_type.into(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ExceptionList {
pub id: String,
pub list_id: String,
pub name: String,
pub description: String,
pub namespace_type: NamespaceType,
#[serde(rename = "type")]
pub list_type: String,
#[serde(rename = "_version", default)]
pub revision: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub os_types: Vec<OsType>,
#[serde(default)]
pub meta: Option<Map<String, Value>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
impl ExceptionList {
pub fn reference(&self) -> ListReference {
ListReference::new(
&self.id,
&self.list_id,
self.namespace_type,
&self.list_type,
)
}
pub fn edit(&self) -> ListEdit {
ListEdit {
id: self.id.clone(),
revision: self.revision.clone(),
namespace_type: self.namespace_type,
list_type: self.list_type.clone(),
name: self.name.clone(),
description: self.description.clone(),
tags: self.tags.clone(),
os_types: self.os_types.clone(),
meta: self.meta.clone(),
version: None,
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct NewList {
name: String,
description: String,
#[serde(rename = "type")]
list_type: String,
namespace_type: NamespaceType,
#[serde(skip_serializing_if = "Option::is_none")]
list_id: Option<String>,
tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
os_types: Vec<OsType>,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
}
impl NewList {
pub fn new(
name: impl Into<String>,
description: impl Into<String>,
list_type: impl Into<String>,
) -> Self {
Self {
name: name.into(),
description: description.into(),
list_type: list_type.into(),
namespace_type: NamespaceType::Single,
list_id: None,
tags: Vec::new(),
os_types: Vec::new(),
version: None,
meta: None,
}
}
pub fn detection(name: impl Into<String>, description: impl Into<String>) -> Self {
Self::new(name, description, "detection")
}
pub fn namespace_type(mut self, namespace: NamespaceType) -> Self {
self.namespace_type = namespace;
self
}
pub fn list_id(mut self, list_id: impl Into<String>) -> Self {
self.list_id = Some(list_id.into());
self
}
pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn os_types(mut self, os_types: Vec<OsType>) -> Self {
self.os_types = os_types;
self
}
pub fn version(mut self, version: u64) -> Self {
self.version = Some(version);
self
}
pub fn meta(mut self, meta: Map<String, Value>) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ListEdit {
id: String,
#[serde(rename = "_version", skip_serializing_if = "Option::is_none")]
revision: Option<String>,
namespace_type: NamespaceType,
#[serde(rename = "type")]
list_type: String,
name: String,
description: String,
tags: Vec<String>,
os_types: Vec<OsType>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<u64>,
}
impl ListEdit {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn os_types(mut self, os_types: Vec<OsType>) -> Self {
self.os_types = os_types;
self
}
pub fn meta(mut self, meta: Map<String, Value>) -> Self {
self.meta = Some(meta);
self
}
pub fn version(mut self, version: u64) -> Self {
self.version = Some(version);
self
}
}
#[derive(Clone, Copy, Debug)]
pub enum ListSelector<'a> {
Id(&'a str),
ListId(&'a str),
}
impl<'a> ListSelector<'a> {
fn pair(self) -> (&'static str, &'a str) {
match self {
Self::Id(id) => ("id", id),
Self::ListId(id) => ("list_id", id),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct ListTarget<'a> {
selector: ListSelector<'a>,
namespace: Option<NamespaceType>,
}
impl<'a> ListTarget<'a> {
pub fn new(selector: ListSelector<'a>, namespace: NamespaceType) -> Self {
Self {
selector,
namespace: Some(namespace),
}
}
fn apply(self, request: Request<'_>) -> Request<'_> {
let request = request.selector(self.selector.pair());
match self.namespace {
Some(namespace) => request.param("namespace_type", namespace.as_str()),
None => request,
}
}
}
impl<'a> From<ListSelector<'a>> for ListTarget<'a> {
fn from(selector: ListSelector<'a>) -> Self {
Self {
selector,
namespace: None,
}
}
}
impl<'a> From<&'a ExceptionList> for ListTarget<'a> {
fn from(list: &'a ExceptionList) -> Self {
Self::new(ListSelector::Id(&list.id), list.namespace_type)
}
}
impl<'a> From<&'a ListReference> for ListTarget<'a> {
fn from(list: &'a ListReference) -> Self {
Self::new(ListSelector::Id(&list.id), list.namespace_type)
}
}
#[derive(Clone, Copy, Debug)]
pub struct ItemsOf<'a> {
list_id: &'a str,
namespace: Option<NamespaceType>,
}
impl<'a> From<&'a str> for ItemsOf<'a> {
fn from(list_id: &'a str) -> Self {
Self {
list_id,
namespace: None,
}
}
}
impl<'a> From<&'a String> for ItemsOf<'a> {
fn from(list_id: &'a String) -> Self {
list_id.as_str().into()
}
}
impl<'a> From<&'a ExceptionList> for ItemsOf<'a> {
fn from(list: &'a ExceptionList) -> Self {
Self {
list_id: &list.list_id,
namespace: Some(list.namespace_type),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum ItemSelector<'a> {
Id(&'a str),
ItemId(&'a str),
}
impl<'a> ItemSelector<'a> {
fn pair(self) -> (&'static str, &'a str) {
match self {
Self::Id(id) => ("id", id),
Self::ItemId(id) => ("item_id", id),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct ItemTarget<'a> {
selector: ItemSelector<'a>,
namespace: Option<NamespaceType>,
}
impl<'a> ItemTarget<'a> {
pub fn new(selector: ItemSelector<'a>, namespace: NamespaceType) -> Self {
Self {
selector,
namespace: Some(namespace),
}
}
fn apply(self, request: Request<'_>) -> Request<'_> {
let request = request.selector(self.selector.pair());
match self.namespace {
Some(namespace) => request.param("namespace_type", namespace.as_str()),
None => request,
}
}
}
impl<'a> From<ItemSelector<'a>> for ItemTarget<'a> {
fn from(selector: ItemSelector<'a>) -> Self {
Self {
selector,
namespace: None,
}
}
}
impl<'a> From<&'a ExceptionItem> for ItemTarget<'a> {
fn from(item: &'a ExceptionItem) -> Self {
Self::new(ItemSelector::Id(&item.id), item.namespace_type)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Operator {
Included,
Excluded,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum NestedEntry {
Match {
field: String,
operator: Operator,
value: String,
},
MatchAny {
field: String,
operator: Operator,
value: Vec<String>,
},
Exists {
field: String,
operator: Operator,
},
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ValueListReference {
pub id: String,
#[serde(rename = "type")]
pub list_type: String,
}
impl ValueListReference {
pub fn new(id: impl Into<String>, list_type: impl Into<String>) -> Self {
Self {
id: id.into(),
list_type: list_type.into(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Entry {
Match {
field: String,
operator: Operator,
value: String,
},
MatchAny {
field: String,
operator: Operator,
value: Vec<String>,
},
Exists {
field: String,
operator: Operator,
},
Wildcard {
field: String,
operator: Operator,
value: String,
},
List {
field: String,
operator: Operator,
list: ValueListReference,
},
Nested {
field: String,
entries: Vec<NestedEntry>,
},
}
#[derive(Clone, Debug, Serialize)]
pub struct Comment {
comment: String,
}
impl Comment {
pub fn new(comment: impl Into<String>) -> Self {
Self {
comment: comment.into(),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ItemComment {
pub id: String,
pub comment: String,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub created_by: Option<String>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Clone, Debug, Serialize)]
pub struct NewItem {
name: String,
description: String,
list_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
item_id: Option<String>,
#[serde(rename = "type")]
item_type: &'static str,
namespace_type: NamespaceType,
entries: Vec<Entry>,
tags: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
os_types: Vec<OsType>,
comments: Vec<Comment>,
#[serde(skip_serializing_if = "Option::is_none")]
expire_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
}
impl NewItem {
pub fn new(list: &ExceptionList, name: impl Into<String>, entries: Vec<Entry>) -> Self {
Self {
name: name.into(),
description: String::new(),
list_id: list.list_id.clone(),
item_id: None,
item_type: "simple",
namespace_type: list.namespace_type,
entries,
tags: Vec::new(),
os_types: Vec::new(),
comments: Vec::new(),
expire_time: None,
meta: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn item_id(mut self, item_id: impl Into<String>) -> Self {
self.item_id = Some(item_id.into());
self
}
pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn os_types(mut self, os_types: Vec<OsType>) -> Self {
self.os_types = os_types;
self
}
pub fn comments(mut self, comments: Vec<Comment>) -> Self {
self.comments = comments;
self
}
pub fn expire_time(mut self, time: impl Into<String>) -> Self {
self.expire_time = Some(time.into());
self
}
pub fn meta(mut self, meta: Map<String, Value>) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ExceptionItem {
pub id: String,
pub item_id: String,
pub list_id: String,
pub name: String,
pub description: String,
pub namespace_type: NamespaceType,
#[serde(rename = "_version", default)]
pub revision: Option<String>,
#[serde(rename = "type", default = "simple")]
pub item_type: String,
pub entries: Vec<Value>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub os_types: Vec<OsType>,
#[serde(default)]
pub comments: Vec<ItemComment>,
#[serde(default)]
pub expire_time: Option<String>,
#[serde(default)]
pub meta: Option<Map<String, Value>>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
fn simple() -> String {
"simple".into()
}
impl ExceptionItem {
pub fn edit(&self) -> ItemEdit {
ItemEdit {
id: self.id.clone(),
revision: self.revision.clone(),
namespace_type: self.namespace_type,
item_type: self.item_type.clone(),
name: self.name.clone(),
description: self.description.clone(),
entries: self.entries.clone(),
tags: self.tags.clone(),
os_types: self.os_types.clone(),
comments: Vec::new(),
expire_time: self.expire_time.clone(),
meta: self.meta.clone(),
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct ItemEdit {
id: String,
#[serde(rename = "_version", skip_serializing_if = "Option::is_none")]
revision: Option<String>,
namespace_type: NamespaceType,
#[serde(rename = "type")]
item_type: String,
name: String,
description: String,
entries: Vec<Value>,
tags: Vec<String>,
os_types: Vec<OsType>,
comments: Vec<Comment>,
#[serde(skip_serializing_if = "Option::is_none")]
expire_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
meta: Option<Map<String, Value>>,
}
impl ItemEdit {
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn entries(mut self, entries: impl IntoIterator<Item = Entry>) -> Self {
self.entries = entries
.into_iter()
.map(|entry| serde_json::to_value(&entry).expect("entries serialize as JSON"))
.collect();
self
}
pub fn tags<I: IntoIterator<Item = S>, S: Into<String>>(mut self, tags: I) -> Self {
self.tags = tags.into_iter().map(Into::into).collect();
self
}
pub fn os_types(mut self, os_types: Vec<OsType>) -> Self {
self.os_types = os_types;
self
}
pub fn add_comment(mut self, comment: impl Into<String>) -> Self {
self.comments.push(Comment::new(comment));
self
}
pub fn expire_time(mut self, time: impl Into<String>) -> Self {
self.expire_time = Some(time.into());
self
}
pub fn meta(mut self, meta: Map<String, Value>) -> Self {
self.meta = Some(meta);
self
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ExceptionPage<T> {
pub data: Vec<T>,
pub page: u32,
pub per_page: u32,
pub total: u64,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub struct ImportResult {
pub success: bool,
pub success_count: u64,
pub errors: Vec<crate::ImportFailure>,
#[serde(flatten)]
pub extra: Map<String, Value>,
}
#[derive(Clone, Copy, Debug)]
pub struct Exceptions<'a>(pub(crate) &'a Kibana);
impl<'a> Exceptions<'a> {
pub fn create_list<B: Serialize + ?Sized>(&self, list: &B) -> CreateList<'a> {
CreateList(
self.0
.request(Method::POST, Scope::Space, &["api", "exception_lists"])
.json(list),
)
}
pub fn get_list<'l>(&self, list: impl Into<ListTarget<'l>>) -> GetList<'a> {
GetList(
list.into().apply(self.0.request(
Method::GET,
Scope::Space,
&["api", "exception_lists"],
)),
)
}
pub fn update_list(&self, list: &ListEdit) -> UpdateList<'a> {
UpdateList(
self.0
.request(Method::PUT, Scope::Space, &["api", "exception_lists"])
.nonempty("_version", list.revision.as_deref().unwrap_or_default())
.json(list),
)
}
pub fn delete_list<'l>(&self, list: impl Into<ListTarget<'l>>) -> DeleteList<'a> {
DeleteList(list.into().apply(self.0.request(
Method::DELETE,
Scope::Space,
&["api", "exception_lists"],
)))
}
pub fn find_lists(&self) -> FindLists<'a> {
FindLists(self.0.request(
Method::GET,
Scope::Space,
&["api", "exception_lists", "_find"],
))
}
pub fn create_item<B: Serialize + ?Sized>(&self, item: &B) -> CreateItem<'a> {
CreateItem(
self.0
.request(
Method::POST,
Scope::Space,
&["api", "exception_lists", "items"],
)
.json(item),
)
}
pub fn get_item<'i>(&self, item: impl Into<ItemTarget<'i>>) -> GetItem<'a> {
GetItem(item.into().apply(self.0.request(
Method::GET,
Scope::Space,
&["api", "exception_lists", "items"],
)))
}
pub fn update_item(&self, item: &ItemEdit) -> UpdateItem<'a> {
UpdateItem(
self.0
.request(
Method::PUT,
Scope::Space,
&["api", "exception_lists", "items"],
)
.nonempty("_version", item.revision.as_deref().unwrap_or_default())
.json(item),
)
}
pub fn delete_item<'i>(&self, item: impl Into<ItemTarget<'i>>) -> DeleteItem<'a> {
DeleteItem(item.into().apply(self.0.request(
Method::DELETE,
Scope::Space,
&["api", "exception_lists", "items"],
)))
}
pub fn find_items<'l>(&self, list: impl Into<ItemsOf<'l>>) -> FindItems<'a> {
let list = list.into();
let request = self
.0
.request(
Method::GET,
Scope::Space,
&["api", "exception_lists", "items", "_find"],
)
.selector(("list_id", list.list_id));
FindItems(match list.namespace {
Some(namespace) => request.param("namespace_type", namespace.as_str()),
None => request,
})
}
pub fn summary<'l>(&self, list: impl Into<ListTarget<'l>>) -> Summary<'a> {
Summary(list.into().apply(self.0.request(
Method::GET,
Scope::Space,
&["api", "exception_lists", "summary"],
)))
}
pub fn duplicate_list(
&self,
list_id: &str,
namespace: NamespaceType,
include_expired: bool,
) -> DuplicateList<'a> {
DuplicateList(
self.0
.request(
Method::POST,
Scope::Space,
&["api", "exception_lists", "_duplicate"],
)
.selector(("list_id", list_id))
.param("namespace_type", namespace.as_str())
.param("include_expired_exceptions", include_expired),
)
}
pub fn export_list(&self, list: &ListReference, include_expired: bool) -> ExportList<'a> {
ExportList(
self.0
.request(
Method::POST,
Scope::Space,
&["api", "exception_lists", "_export"],
)
.param("id", &list.id)
.param("list_id", &list.list_id)
.param("namespace_type", list.namespace_type.as_str())
.param("include_expired_exceptions", include_expired),
)
}
pub fn import_lists(&self, ndjson: impl Into<Vec<u8>>) -> ImportLists<'a> {
ImportLists(
self.0
.request(
Method::POST,
Scope::Space,
&["api", "exception_lists", "_import"],
)
.file("exceptions.ndjson", "application/ndjson", ndjson.into()),
)
}
}
macro_rules! namespace_setter {
($($name:ident),*) => {$(
impl $name<'_> {
pub fn namespace_type(self, namespace: NamespaceType) -> Self {
Self(self.0.param("namespace_type", namespace.as_str()))
}
}
)*};
}
macro_rules! find_setters {
($($name:ident),*) => {$(
impl $name<'_> {
pub fn page(self, page: u32) -> Self {
Self(self.0.positive_param("page", page))
}
pub fn per_page(self, per_page: u32) -> Self {
Self(self.0.positive_param("per_page", per_page))
}
pub fn filter(self, filter: &str) -> Self {
Self(self.0.param("filter", filter))
}
pub fn sort_field(self, field: &str) -> Self {
Self(self.0.param("sort_field", field))
}
pub fn sort_order(self, order: SortOrder) -> Self {
Self(self.0.param("sort_order", order.as_str()))
}
}
)*};
}
endpoint! {
CreateList => ExceptionList
}
endpoint! {
GetList => ExceptionList
}
endpoint! {
UpdateList => ExceptionList
}
endpoint! {
DeleteList => ExceptionList
}
endpoint! {
FindLists => ExceptionPage<ExceptionList>
}
endpoint! {
CreateItem => ExceptionItem
}
endpoint! {
GetItem => ExceptionItem
}
endpoint! {
UpdateItem => ExceptionItem
}
endpoint! {
DeleteItem => ExceptionItem
}
endpoint! {
FindItems => ExceptionPage<ExceptionItem>
}
impl FindItems<'_> {
pub fn search(self, text: &str) -> Self {
Self(self.0.param("search", text))
}
}
endpoint! {
Summary => Value
}
impl Summary<'_> {
pub fn filter(self, filter: &str) -> Self {
Self(self.0.param("filter", filter))
}
}
endpoint! {
DuplicateList => ExceptionList
}
endpoint! {
ExportList => Raw
}
endpoint! {
ImportLists => ImportResult
}
impl ImportLists<'_> {
pub fn overwrite(self, overwrite: bool) -> Self {
Self(self.0.param("overwrite", overwrite))
}
pub fn as_new_list(self, enabled: bool) -> Self {
Self(self.0.param("as_new_list", enabled))
}
}
namespace_setter!(
GetList, DeleteList, FindLists, GetItem, DeleteItem, FindItems, Summary
);
find_setters!(FindLists, FindItems);
paginated!(
FindLists => ExceptionPage<ExceptionList>,
FindItems => ExceptionPage<ExceptionItem>,
);