diff --git a/Cargo.toml b/Cargo.toml index 4cd76d2e..d7a6606c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ bcrypt = "0.15.0" bytes = "1.5.0" clap = { version = "4.5.4", features = ["env", "derive", "color"] } -sqlx = { git = "https://github.com/launchbadge/sqlx", default-features = true, features = ["runtime-tokio", "sqlite", "tls-rustls"] } +sqlx = { git = "https://github.com/launchbadge/sqlx", default-features = true, features = ["runtime-tokio", "sqlite", "tls-rustls", "json"] } filetime = "0.2.23" glob = "0.3.1" diff --git a/resources/sqlite/migrations/0005_EventComments.sql b/resources/sqlite/migrations/0005_EventComments.sql new file mode 100644 index 00000000..a5a1bd86 --- /dev/null +++ b/resources/sqlite/migrations/0005_EventComments.sql @@ -0,0 +1,3 @@ +ALTER TABLE events + ADD COLUMN history JSON + default '[]'; diff --git a/src/elastic/eventrepo.rs b/src/elastic/eventrepo.rs index 597c0fc3..9ec411d3 100644 --- a/src/elastic/eventrepo.rs +++ b/src/elastic/eventrepo.rs @@ -5,17 +5,12 @@ use super::query_string_query; use super::Client; use super::ElasticError; use super::HistoryEntry; -use super::ACTION_ARCHIVED; -use super::ACTION_COMMENT; +use super::HistoryEntryBuilder; use super::TAG_ESCALATED; use crate::datetime; -use crate::datetime::DateTime; use crate::elastic::importer::ElasticEventSink; use crate::elastic::request::exists_filter; -use crate::elastic::{ - request, ElasticResponse, ACTION_DEESCALATED, ACTION_ESCALATED, TAGS_ARCHIVED, TAGS_ESCALATED, - TAG_ARCHIVED, -}; +use crate::elastic::{request, ElasticResponse, TAGS_ARCHIVED, TAGS_ESCALATED, TAG_ARCHIVED}; use crate::eventrepo::{self, DatastoreError}; use crate::queryparser; use crate::queryparser::QueryElement; @@ -292,12 +287,7 @@ impl ElasticEventRepo { } } }); - let action = HistoryEntry { - username: "anonymous".to_string(), - timestamp: datetime::DateTime::now().to_elastic(), - action: ACTION_ARCHIVED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_archive().build(); self.add_tag_by_query(query, TAG_ARCHIVED, &action).await } @@ -309,12 +299,7 @@ impl ElasticEventRepo { } } }); - let action = HistoryEntry { - username: "anonymous".to_string(), - timestamp: datetime::DateTime::now().to_elastic(), - action: ACTION_ESCALATED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_escalate().build(); self.add_tag_by_query(query, TAG_ESCALATED, &action).await } @@ -326,12 +311,7 @@ impl ElasticEventRepo { } } }); - let action = HistoryEntry { - username: "anonymous".to_string(), - timestamp: datetime::DateTime::now().to_elastic(), - action: ACTION_DEESCALATED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_deescalate().build(); self.remove_tag_by_query(query, TAG_ESCALATED, &action) .await } @@ -340,7 +320,7 @@ impl ElasticEventRepo { &self, event_id: &str, comment: String, - username: &str, + session: Arc, ) -> Result<(), DatastoreError> { let query = json!({ "bool": { @@ -349,12 +329,10 @@ impl ElasticEventRepo { } } }); - let action = HistoryEntry { - username: username.to_string(), - timestamp: datetime::DateTime::now().to_elastic(), - action: ACTION_COMMENT.to_string(), - comment: Some(comment), - }; + let action = HistoryEntryBuilder::new_comment() + .username(session.username.clone()) + .comment(comment) + .build(); self.add_tags_by_query(query, &[], &action).await } @@ -487,12 +465,7 @@ impl ElasticEventRepo { &self, alert_group: api::AlertGroupSpec, ) -> Result<(), DatastoreError> { - let action = HistoryEntry { - username: "anonymous".to_string(), - timestamp: DateTime::now().to_elastic(), - action: ACTION_ARCHIVED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_archive().build(); self.add_tags_by_alert_group(alert_group, &TAGS_ARCHIVED, &action) .await } @@ -502,13 +475,9 @@ impl ElasticEventRepo { alert_group: api::AlertGroupSpec, session: Arc, ) -> Result<(), DatastoreError> { - let action = HistoryEntry { - username: session.username().to_string(), - //username: "anonymous".to_string(), - timestamp: DateTime::now().to_elastic(), - action: ACTION_ESCALATED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_escalate() + .username(session.username.clone()) + .build(); self.add_tags_by_alert_group(alert_group, &TAGS_ESCALATED, &action) .await } @@ -517,12 +486,7 @@ impl ElasticEventRepo { &self, alert_group: api::AlertGroupSpec, ) -> Result<(), DatastoreError> { - let action = HistoryEntry { - username: "anonymous".to_string(), - timestamp: DateTime::now().to_elastic(), - action: ACTION_DEESCALATED.to_string(), - comment: None, - }; + let action = HistoryEntryBuilder::new_deescalate().build(); self.remove_tags_by_alert_group(alert_group, &TAGS_ESCALATED, &action) .await } @@ -629,21 +593,6 @@ impl ElasticEventRepo { Ok(response) } - pub async fn comment_by_alert_group( - &self, - alert_group: api::AlertGroupSpec, - comment: String, - username: &str, - ) -> Result<(), DatastoreError> { - let entry = HistoryEntry { - username: username.to_string(), - timestamp: DateTime::now().to_elastic(), - action: ACTION_COMMENT.to_string(), - comment: Some(comment), - }; - self.add_tags_by_alert_group(alert_group, &[], &entry).await - } - async fn get_earliest_timestamp( &self, ) -> Result, DatastoreError> { diff --git a/src/elastic/eventrepo/alerts.rs b/src/elastic/eventrepo/alerts.rs index c8b3457b..9d140b0e 100644 --- a/src/elastic/eventrepo/alerts.rs +++ b/src/elastic/eventrepo/alerts.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT use axum::{response::IntoResponse, Json}; -use tracing::{error, info, warn}; +use tracing::{debug, error, warn}; use crate::{ elastic::{AlertQueryOptions, ElasticResponse}, @@ -163,7 +163,7 @@ impl ElasticEventRepo { return Err(DatastoreError::ElasticSearchError(error.first_reason())); } - info!( + debug!( "Elasticsearch alert query took {:?}, es-time: {}, response-size: {}", start.elapsed(), response.took, diff --git a/src/elastic/mod.rs b/src/elastic/mod.rs index 96598d4e..690d2a41 100644 --- a/src/elastic/mod.rs +++ b/src/elastic/mod.rs @@ -16,15 +16,28 @@ pub mod eventrepo; pub mod importer; pub mod request; -pub const ACTION_ARCHIVED: &str = "archived"; -pub const ACTION_ESCALATED: &str = "escalated"; -pub const ACTION_DEESCALATED: &str = "de-escalated"; -pub const ACTION_COMMENT: &str = "comment"; +pub(crate) const TAG_ESCALATED: &str = "evebox.escalated"; +pub(crate) const TAGS_ESCALATED: [&str; 1] = [TAG_ESCALATED]; +pub(crate) const TAG_ARCHIVED: &str = "evebox.archived"; +pub(crate) const TAGS_ARCHIVED: [&str; 1] = [TAG_ARCHIVED]; + +pub(crate) enum HistoryType { + Archived, + Escalated, + Deescalated, + Comment, +} -pub const TAG_ESCALATED: &str = "evebox.escalated"; -pub const TAGS_ESCALATED: [&str; 1] = [TAG_ESCALATED]; -pub const TAG_ARCHIVED: &str = "evebox.archived"; -pub const TAGS_ARCHIVED: [&str; 1] = [TAG_ARCHIVED]; +impl std::fmt::Display for HistoryType { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + HistoryType::Archived => write!(f, "archived"), + HistoryType::Escalated => write!(f, "escalated"), + HistoryType::Deescalated => write!(f, "de-escalated"), + HistoryType::Comment => write!(f, "comment"), + } + } +} #[derive(Debug, Error)] pub enum ElasticError { @@ -50,13 +63,73 @@ pub(crate) struct AlertQueryOptions { #[derive(Serialize)] pub(crate) struct HistoryEntry { - pub username: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, pub timestamp: String, pub action: String, #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, } +impl HistoryEntry { + pub(crate) fn to_json(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +pub(crate) struct HistoryEntryBuilder { + timestamp: DateTime, + action: String, + username: Option, + comment: Option, +} + +impl HistoryEntryBuilder { + fn new(action: HistoryType) -> Self { + Self { + action: action.to_string(), + timestamp: DateTime::now(), + username: None, + comment: None, + } + } + + pub fn new_archive() -> Self { + Self::new(HistoryType::Archived) + } + + pub fn new_escalate() -> Self { + Self::new(HistoryType::Escalated) + } + + pub fn new_deescalate() -> Self { + Self::new(HistoryType::Deescalated) + } + + pub fn new_comment() -> Self { + Self::new(HistoryType::Comment) + } + + pub fn username(mut self, username: Option>) -> Self { + self.username = username.map(|u| u.into()); + self + } + + pub fn comment(mut self, comment: impl Into) -> Self { + self.comment = Some(comment.into()); + self + } + + pub fn build(self) -> HistoryEntry { + HistoryEntry { + username: self.username, + timestamp: self.timestamp.to_rfc3339_utc(), + action: self.action, + comment: self.comment, + } + } +} + pub fn query_string_query(query_string: &str) -> serde_json::Value { let escaped = query_string .replace('\\', "\\\\") diff --git a/src/eve/eve.rs b/src/eve/eve.rs index 90c8a824..63a09304 100644 --- a/src/eve/eve.rs +++ b/src/eve/eve.rs @@ -18,7 +18,7 @@ impl Eve for serde_json::Value { } } -pub fn add_evebox_metadata(event: &mut serde_json::Value, filename: Option) { +pub(crate) fn add_evebox_metadata(event: &mut serde_json::Value, filename: Option) { if let serde_json::Value::Null = event["evebox"] { event["evebox"] = serde_json::json!({}); } @@ -31,3 +31,12 @@ pub fn add_evebox_metadata(event: &mut serde_json::Value, filename: Option Result<(), DatastoreError> { match self { EventRepo::Elastic(ds) => ds.escalate_by_alert_group(alert_group, session).await, - EventRepo::SQLite(ds) => ds.escalate_by_alert_group(alert_group).await, + EventRepo::SQLite(ds) => ds.escalate_by_alert_group(session, alert_group).await, _ => Err(DatastoreError::Unimplemented), } } pub async fn deescalate_by_alert_group( &self, + session: Arc, alert_group: api::AlertGroupSpec, ) -> Result<(), DatastoreError> { match self { EventRepo::Elastic(ds) => ds.deescalate_by_alert_group(alert_group).await, - EventRepo::SQLite(ds) => ds.deescalate_by_alert_group(alert_group).await, - _ => Err(DatastoreError::Unimplemented), - } - } - - pub async fn comment_by_alert_group( - &self, - alert_group: api::AlertGroupSpec, - comment: String, - username: &str, - ) -> Result<(), DatastoreError> { - match self { - EventRepo::Elastic(ds) => { - ds.comment_by_alert_group(alert_group, comment, username) - .await - } + EventRepo::SQLite(ds) => ds.deescalate_by_alert_group(session, alert_group).await, _ => Err(DatastoreError::Unimplemented), } } @@ -185,10 +172,11 @@ impl EventRepo { &self, event_id: &str, comment: String, - username: &str, + session: Arc, ) -> Result<(), DatastoreError> { match self { - EventRepo::Elastic(ds) => ds.comment_event_by_id(event_id, comment, username).await, + EventRepo::Elastic(ds) => ds.comment_event_by_id(event_id, comment, session).await, + EventRepo::SQLite(ds) => ds.comment_event_by_id(event_id, comment, session).await, _ => Err(DatastoreError::Unimplemented), } } diff --git a/src/server/api/api.rs b/src/server/api/api.rs index 026928e7..2714fe79 100644 --- a/src/server/api/api.rs +++ b/src/server/api/api.rs @@ -52,7 +52,7 @@ pub(crate) async fn config( pub(crate) async fn get_user(SessionExtractor(session): SessionExtractor) -> impl IntoResponse { let user = json!({ - "username": session.username(), + "username": session.username, }); Json(user) } @@ -133,13 +133,13 @@ pub(crate) async fn alert_group_star( pub(crate) async fn alert_group_unstar( Extension(context): Extension>, - SessionExtractor(_session): SessionExtractor, + SessionExtractor(session): SessionExtractor, Json(request): Json, ) -> impl IntoResponse { info!("De-escalating alert group: {:?}", request); context .datastore - .deescalate_by_alert_group(request) + .deescalate_by_alert_group(session, request) .await .unwrap(); StatusCode::OK @@ -350,7 +350,7 @@ pub(crate) async fn comment_by_event_id( ) -> impl IntoResponse { match context .datastore - .comment_event_by_id(&event_id, body.comment.to_string(), session.username()) + .comment_event_by_id(&event_id, body.comment.to_string(), session) .await { Ok(()) => StatusCode::OK, @@ -408,30 +408,6 @@ pub(crate) struct EventCommentRequestBody { pub comment: String, } -#[derive(Deserialize)] -pub(crate) struct AlertGroupCommentRequest { - pub alert_group: AlertGroupSpec, - pub comment: String, -} - -pub(crate) async fn alert_group_comment( - Extension(context): Extension>, - SessionExtractor(session): SessionExtractor, - Json(request): Json, -) -> impl IntoResponse { - match context - .datastore - .comment_by_alert_group(request.alert_group, request.comment, session.username()) - .await - { - Ok(()) => StatusCode::OK, - Err(err) => { - info!("Failed to apply command to alert-group: {:?}", err); - StatusCode::INTERNAL_SERVER_ERROR - } - } -} - fn parse_then_from_duration( now: &crate::datetime::DateTime, duration: &str, diff --git a/src/server/api/login.rs b/src/server/api/login.rs index d3acb8d1..243b9f1a 100644 --- a/src/server/api/login.rs +++ b/src/server/api/login.rs @@ -110,7 +110,7 @@ pub(crate) async fn logout( if !context.session_store.delete(session_id) { warn!("Logout request for unknown session ID"); } else { - info!("User logged out: {:}", session.username()); + info!("User logged out: {:?}", session.username); } } StatusCode::OK diff --git a/src/server/main.rs b/src/server/main.rs index c8edb68c..fc20eeb9 100644 --- a/src/server/main.rs +++ b/src/server/main.rs @@ -343,10 +343,9 @@ pub(crate) fn build_axum_service( .route("/api/1/alert-group/star", post(api::alert_group_star)) .route("/api/1/alert-group/unstar", post(api::alert_group_unstar)) .route("/api/1/alert-group/archive", post(api::alert_group_archive)) - .route("/api/1/alert-group/comment", post(api::alert_group_comment)) .route("/api/1/event/:id/archive", post(api::archive_event_by_id)) .route("/api/1/event/:id/escalate", post(api::escalate_event_by_id)) - .route("/api/1/event/:id/comment", post(api::comment_by_event_id)) + .route("/api/event/:id/comment", post(api::comment_by_event_id)) .route( "/api/1/event/:id/de-escalate", post(api::deescalate_event_by_id), diff --git a/src/server/session.rs b/src/server/session.rs index 5a846e43..6d55badf 100644 --- a/src/server/session.rs +++ b/src/server/session.rs @@ -75,13 +75,13 @@ impl Session { } } - pub fn username(&self) -> &str { - if let Some(username) = &self.username { - username - } else { - "" - } - } + // pub fn username(&self) -> &str { + // if let Some(username) = &self.username { + // username + // } else { + // "" + // } + // } } pub(crate) fn generate_session_id() -> String { diff --git a/src/sqlite/eventrepo.rs b/src/sqlite/eventrepo.rs index ce564790..9553b3e6 100644 --- a/src/sqlite/eventrepo.rs +++ b/src/sqlite/eventrepo.rs @@ -2,8 +2,11 @@ // SPDX-License-Identifier: MIT use crate::datetime::DateTime; +use crate::elastic::HistoryEntryBuilder; +use crate::eve::eve::ensure_has_history; use crate::eventrepo::DatastoreError; use crate::server::api::AlertGroupSpec; +use crate::server::session::Session; use crate::sqlite::log_query_plan; use crate::{LOG_QUERIES, LOG_QUERY_PLAN}; use serde_json::json; @@ -12,12 +15,13 @@ use sqlx::Arguments; use sqlx::{Row, SqliteConnection, SqlitePool}; use std::sync::Arc; use std::time::Instant; -use tracing::{debug, info, instrument}; +use tracing::{debug, info, instrument, warn}; use super::has_table; mod agg; mod alerts; +mod comments; mod dhcp; mod events; mod stats; @@ -108,7 +112,11 @@ impl SqliteEventRepo { &self, event_id: String, ) -> Result, DatastoreError> { - let sql = "SELECT rowid, archived, escalated, source FROM events WHERE rowid = ?"; + let sql = r#" + SELECT + rowid, archived, escalated, source, history + FROM events + WHERE rowid = ?"#; if *LOG_QUERY_PLAN { log_query_plan(&self.pool, sql, &SqliteArguments::default()).await; @@ -123,6 +131,7 @@ impl SqliteEventRepo { let archived: i8 = row.try_get(1)?; let escalated: i8 = row.try_get(2)?; let mut parsed: serde_json::Value = row.try_get(3)?; + let history: serde_json::Value = row.try_get("history")?; if let serde_json::Value::Null = &parsed["tags"] { let tags: Vec = Vec::new(); @@ -138,6 +147,9 @@ impl SqliteEventRepo { } } + ensure_has_history(&mut parsed); + parsed["evebox"]["history"] = history; + let response = json!({ "_id": rowid, "_source": parsed, @@ -154,16 +166,21 @@ impl SqliteEventRepo { alert_group: AlertGroupSpec, ) -> Result<(), DatastoreError> { debug!("Archiving alert group: {:?}", alert_group); + + let action = HistoryEntryBuilder::new_archive().build(); let now = Instant::now(); let sql = " UPDATE events - SET archived = 1 + SET archived = 1, + history = json_insert(history, '$[#]', json(?)) WHERE %WHERE% "; let mut args = SqliteArguments::default(); let mut filters: Vec = Vec::new(); + args.add(action.to_json()); + filters.push("json_extract(events.source, '$.event_type') = 'alert'".to_string()); filters.push("archived = 0".to_string()); @@ -208,12 +225,21 @@ impl SqliteEventRepo { let mut filters: Vec = Vec::new(); let mut args = SqliteArguments::default(); + let action = if escalate { + HistoryEntryBuilder::new_escalate() + } else { + HistoryEntryBuilder::new_deescalate() + } + .build(); + let sql = " UPDATE events - SET escalated = ? + SET escalated = ?, + history = json_insert(history, '$[#]', json(?)) WHERE %WHERE% "; args.add(if escalate { 1 } else { 0 }); + args.add(serde_json::to_string(&action).unwrap()); filters.push("json_extract(events.source, '$.event_type') = 'alert'".to_string()); filters.push("escalated = ?".to_string()); @@ -242,13 +268,21 @@ impl SqliteEventRepo { log_query_plan(&self.pool, &sql, &args).await; } + let start = Instant::now(); let r = sqlx::query_with(&sql, args).execute(&self.pool).await?; let n = r.rows_affected(); + info!( + "Set {} events to escalated = {} in {:?}", + n, + escalate, + start.elapsed() + ); Ok(n) } pub async fn escalate_by_alert_group( &self, + _session: Arc, alert_group: AlertGroupSpec, ) -> Result<(), DatastoreError> { let n = self @@ -260,6 +294,7 @@ impl SqliteEventRepo { pub async fn deescalate_by_alert_group( &self, + _session: Arc, alert_group: AlertGroupSpec, ) -> Result<(), DatastoreError> { let n = self @@ -270,18 +305,25 @@ impl SqliteEventRepo { } pub async fn archive_event_by_id(&self, event_id: &str) -> Result<(), DatastoreError> { - let sql = "UPDATE events SET archived = 1 WHERE rowid = ?"; + let action = HistoryEntryBuilder::new_archive().build(); + let sql = r#" + UPDATE events + SET archived = 1, + history = json_insert(history, '$[#]', json(?)) + WHERE rowid = ?"#; if *LOG_QUERY_PLAN { log_query_plan(&self.pool, sql, &SqliteArguments::default()).await; } let n = sqlx::query(sql) + .bind(action.to_json()) .bind(event_id) .execute(&self.pool) .await? .rows_affected(); if n == 0 { + warn!("Archive by event ID request did not update any events"); Err(DatastoreError::EventNotFound) } else { Ok(()) @@ -293,7 +335,18 @@ impl SqliteEventRepo { event_id: &str, escalate: bool, ) -> Result<(), DatastoreError> { - let sql = "UPDATE events SET escalated = ? WHERE rowid = ?"; + let action = if escalate { + HistoryEntryBuilder::new_escalate() + } else { + HistoryEntryBuilder::new_deescalate() + } + .build(); + + let sql = r#" + UPDATE events + SET escalated = ?, + history = json_insert(history, '$[#]', json(?)) + WHERE rowid = ?"#; if *LOG_QUERY_PLAN { log_query_plan(&self.pool, sql, &SqliteArguments::default()).await; @@ -301,6 +354,7 @@ impl SqliteEventRepo { let n = sqlx::query(sql) .bind(if escalate { 1 } else { 0 }) + .bind(action.to_json()) .bind(event_id) .execute(&self.pool) .await? diff --git a/src/sqlite/eventrepo/alerts.rs b/src/sqlite/eventrepo/alerts.rs index a7d6f724..90c0e7fc 100644 --- a/src/sqlite/eventrepo/alerts.rs +++ b/src/sqlite/eventrepo/alerts.rs @@ -41,6 +41,7 @@ impl SqliteEventRepo { .select("timestamp") .select("escalated") .select("archived") + .select("history") .selectjs("alert.signature_id") .selectjs("alert.signature") .selectjs("alert.severity") diff --git a/src/sqlite/eventrepo/comments.rs b/src/sqlite/eventrepo/comments.rs new file mode 100644 index 00000000..71e72090 --- /dev/null +++ b/src/sqlite/eventrepo/comments.rs @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: (C) 2024 Jason Ish +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use sqlx::Connection; +use tracing::warn; + +use crate::{elastic::HistoryEntryBuilder, eventrepo::DatastoreError, server::session::Session}; + +use super::SqliteEventRepo; + +impl SqliteEventRepo { + pub async fn comment_event_by_id( + &self, + event_id: &str, + comment: String, + session: Arc, + ) -> Result<(), DatastoreError> { + let event_id: i64 = event_id.parse()?; + let action = HistoryEntryBuilder::new_comment() + .username(session.username.clone()) + .comment(comment) + .build(); + let mut conn = self.writer.lock().await; + let mut tx = conn.begin().await?; + + let sql = r#" + UPDATE events + SET history = json_insert(history, '$[#]', json(?)) + WHERE rowid = ?"#; + + let n = sqlx::query(sql) + .bind(action.to_json()) + .bind(event_id) + .execute(&mut *tx) + .await? + .rows_affected(); + + tx.commit().await?; + + if n == 0 { + warn!("Archive by event ID request did not update any events"); + Err(DatastoreError::EventNotFound) + } else { + Ok(()) + } + } +} diff --git a/webapp/src/Alerts.tsx b/webapp/src/Alerts.tsx index 8897c930..dcffcd1b 100644 --- a/webapp/src/Alerts.tsx +++ b/webapp/src/Alerts.tsx @@ -574,9 +574,7 @@ export function Alerts() { eventStore.viewOffset = getOffset(); eventStore.cursor = cursor(); console.log(`EVENT_STORE.active._id=${eventStore.active?._id}`); - console.log(JSON.stringify(event._metadata)); - const _metadata = encodeURIComponent(JSON.stringify(event._metadata)); - navigate(`${location.pathname}/${event._id}?_metadata=${_metadata}`, { + navigate(`${location.pathname}/${event._id}`, { state: { referer: location.pathname, }, diff --git a/webapp/src/EventView.tsx b/webapp/src/EventView.tsx index 54854056..79456fcb 100644 --- a/webapp/src/EventView.tsx +++ b/webapp/src/EventView.tsx @@ -1,26 +1,22 @@ // SPDX-FileCopyrightText: (C) 2023 Jason Ish // SPDX-License-Identifier: MIT -import { - A, - useLocation, - useNavigate, - useParams, - useSearchParams, -} from "@solidjs/router"; +import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; import { Top } from "./Top"; import { createEffect, createSignal, + createUniqueId, For, Match, onCleanup, onMount, + Setter, Show, Switch, untrack, } from "solid-js"; -import { API, getEventById } from "./api"; +import { API, archiveEvent, getEventById, postComment } from "./api"; import { archiveAggregateAlert } from "./api"; import { Button, @@ -37,13 +33,7 @@ import { AggregateAlert, EcsGeo, EveDns, Event, EventWrapper } from "./types"; import { parse_timestamp } from "./datetime"; import { formatAddressWithPort, formatEventDescription } from "./formatters"; import tinykeys from "tinykeys"; -import { - eventIsArchived, - eventIsEscalated, - eventSetArchived, - eventSetEscalated, - eventUnsetEscalated, -} from "./event"; +import { eventIsArchived, eventIsEscalated, eventSetArchived } from "./event"; import { eventStore } from "./eventstore"; import { addNotification } from "./Notifications"; import { eventNameFromType } from "./Events"; @@ -70,6 +60,7 @@ export function EventView() { const [commonDetails, setCommonDetails] = createSignal(); const [showCopyToast, setShowCopyToast] = createSignal(false); const [history, setHistory] = createSignal([]); + const [showCommentForm, setShowCommentForm] = createSignal(false); const [geoIp, setGeoIp] = createStore<{ source: EcsGeo | undefined; destination: EcsGeo | undefined; @@ -170,25 +161,27 @@ export function EventView() { console.log(`-- Requested event ID: ${params.id}`); console.log(`-- Active event ID: ${eventStore.active?._id}`); console.log(`-- Events in store: ${eventStore.events.length}`); - - console.log("Event.createEffect: Fetching event by ID: " + params.id); - getEventById(params.id) - .then((event) => { - if (eventStore.active && eventStore.active._id == params.id) { - // Copy (by reference) the metadata and tags from the partial - // event in the store so the archive and escalation states are - // reflected when the user clicks back to the alerts view. - event._metadata = eventStore.active._metadata; - event._source.tags = eventStore.active._source.tags; - } - setEvent(event); - }) - .catch(() => { - setEvent(undefined); - }); + refreshEvent(); }); }); + const refreshEvent = () => { + getEventById(params.id) + .then((event) => { + if (eventStore.active && eventStore.active._id == params.id) { + // Copy (by reference) the metadata and tags from the partial + // event in the store so the archive and escalation states are + // reflected when the user clicks back to the alerts view. + event._metadata = eventStore.active._metadata; + event._source.tags = eventStore.active._source.tags; + } + setEvent(event); + }) + .catch(() => { + setEvent(undefined); + }); + }; + createEffect(() => { let source = event()?._source; @@ -387,36 +380,36 @@ export function EventView() { const alert = event() as AggregateAlert; archiveAggregateAlert(alert).then(() => {}); eventSetArchived(alert); + } else if (event()) { + archiveEvent(event()!); } goBack(); } - function escalate() { + async function escalate() { let ev = event(); if (ev) { if (isAggregateAlert()) { - void API.escalateAggregateAlert(ev); + await API.escalateAggregateAlert(ev); ev._metadata!.escalated_count = ev._metadata!.count; } else { - void API.escalateEvent(ev); + await API.escalateEvent(ev); } - eventSetEscalated(ev); - setEvent({ ...ev }); + refreshEvent(); } } - function deEscalate() { + async function deEscalate() { let ev = event(); if (ev) { if (isAggregateAlert()) { - void API.deEscalateAggregateAlert(ev); + await API.deEscalateAggregateAlert(ev); ev._metadata!.escalated_count = 0; } else { - void API.deEscalateEvent(ev); + await API.deEscalateEvent(ev); } - eventUnsetEscalated(ev); - setEvent({ ...ev }); + refreshEvent(); } } @@ -701,7 +694,22 @@ export function EventView() { - + + + {/* Never show if there is history, as it embeds a comment form. */} + + setShowCommentForm(false)} + /> + {/* GeoIP */} @@ -1407,19 +1415,37 @@ function StatsCard(props: { stats: { [key: string]: any } }) { ); } -function History(props: any) { +function History(props: { + eventId: string | number; + history: any[]; + onChange: () => void; + setShowCommentForm: Setter; + showCommentForm: boolean; +}) { + const inputId = createUniqueId(); + + const submitEvent = () => { + let comment = (document.getElementById(inputId) as HTMLInputElement).value; + postComment(props.eventId, comment).then(() => { + props.onChange(); + }); + + // Clear the comment. + (document.getElementById(inputId) as HTMLInputElement).value = ""; + }; + return ( - 0}> +
History
-
- - {(entry) => ( - <> -
-
+
+
    + + {(entry) => ( + <> +
  • {formatTimestamp(entry.timestamp).slice(0, -4)} {" - "} @@ -1429,17 +1455,89 @@ function History(props: any) { De-escalated + + Comment + {" "} - by {entry.username} -
-
- - )} - + by {entry.username || "null"} + +

{entry.comment}

+
+ + + )} + +
+ + +
); } + +function CommentEntry(props: { + eventId: string | number; + onChange: () => void; + close: () => void; +}) { + const inputId = createUniqueId(); + + const submitEvent = () => { + let comment = (document.getElementById(inputId) as HTMLInputElement).value; + postComment(props.eventId, comment).then(() => { + props.onChange(); + }); + + // Clear the comment. + (document.getElementById(inputId) as HTMLInputElement).value = ""; + + // Close the comment form. + props.close(); + }; + + return ( + <> +
+
+
+
+
Comment
+
+