From d0354d6e966671dd945bed1b4594bf5e61f590d8 Mon Sep 17 00:00:00 2001 From: "octoaide[bot]" <204759324+octoaide[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:00:27 -0700 Subject: [PATCH 1/2] Update CHANGELOG.md and 8 other files --- CHANGELOG.md | 12 +- Cargo.toml | 2 +- src/graphql/event.rs | 136 ++++++++++++++++-- src/graphql/event/conn.rs | 36 ++--- src/graphql/event/ftp.rs | 12 +- src/graphql/event/http.rs | 12 +- src/graphql/event/ldap.rs | 12 +- src/graphql/event/rdp.rs | 12 +- .../event/unusual_destination_pattern.rs | 12 +- 9 files changed, 187 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f71d1b9e..a66fcd65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed -- Bumped `review-database` dependency to commit `bd30664`, which renames +- Bumped `review-database` dependency to commit `644db00`, which renames several fields used internally by this crate: `Agent.node` and `ExternalService.node` are now `node_id`, `TrafficFilter.agent` is now `host_fqdn`, and `BlocklistKerberos.client_name` / `service_name` are @@ -28,6 +28,16 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm `hostFqdn`, and the `agents` argument on the `trafficFilterList` query and the `applyTrafficFilterRules` mutation is now `hostFqdns`. - `BlocklistKerberos.clientName` / `serviceName` are now `cname` / `sname`. +- Renamed `startTime` / `endTime` on the `PortScan`, `MultiHostPortScan`, + `ExternalDdos`, `FtpBruteForce`, `LdapBruteForce`, `RdpBruteForce`, and + `RepeatedHttpSessions` GraphQL types to `firstEventStartTime` / + `lastEventStartTime`. These fields are the session start times of the first + and last raw events used for detection. This is a breaking schema change; + clients must migrate to the new field names. +- Renamed `startTime` / `endTime` on `UnusualDestinationPattern` to + `samplingWindowStartTime` / `samplingWindowEndTime`, the boundaries of the + anomaly sampling window used for detection. This is a breaking schema + change; clients must migrate to the new field names. ### Added diff --git a/Cargo.toml b/Cargo.toml index d7b49fdb..45e11e54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ num-traits = "0.2" reqwest = { version = "0.12", default-features = false, features = [ "rustls-tls-native-roots", ] } -review-database = { git = "https://github.com/aicers/review-database.git", rev = "1e943dc" } +review-database = { git = "https://github.com/aicers/review-database.git", rev = "644db00" } roxy = { git = "https://github.com/aicers/roxy.git", tag = "0.6.0" } rustls = { version = "0.23", default-features = false, features = [ "ring", diff --git a/src/graphql/event.rs b/src/graphql/event.rs index 7eb038d1..f81953f5 100644 --- a/src/graphql/event.rs +++ b/src/graphql/event.rs @@ -2272,14 +2272,15 @@ mod tests { .unwrap() .and_local_timezone(Utc) .unwrap(); + let first_event_start_time = ts - chrono::Duration::minutes(5); let fields = MultiHostPortScanFields { sensor: "sensor1".to_string(), orig_addr: Ipv4Addr::from(1).into(), resp_port: 443, resp_addrs: vec![Ipv4Addr::from(2).into(), Ipv4Addr::from(3).into()], proto: 6, - start_time: ts.timestamp_nanos_opt().unwrap(), - end_time: ts.timestamp_nanos_opt().unwrap(), + first_event_start_time: first_event_start_time.timestamp_nanos_opt().unwrap(), + last_event_start_time: ts.timestamp_nanos_opt().unwrap(), confidence: 0.8, category: Some(EventCategory::CommandAndControl), }; @@ -2294,15 +2295,116 @@ mod tests { let query = format!( "{{ event(id: \"{key}\") {{ \ - ... on MultiHostPortScan {{ origCountry respCountries }} \ + ... on MultiHostPortScan {{ \ + origCountry \ + respCountries \ + firstEventStartTime \ + lastEventStartTime \ + }} \ }} }}" ); let res = schema.execute_as_system_admin(&query).await; + let data = res.data.to_string(); + assert!( + data.contains(&format!( + "firstEventStartTime: \"{}\"", + first_event_start_time.to_rfc3339() + )), + "data: {data}" + ); + assert!( + data.contains(&format!("lastEventStartTime: \"{}\"", ts.to_rfc3339())), + "data: {data}" + ); + assert!(data.contains(r#"origCountry: "ZZ""#), "data: {data}"); + assert!( + data.contains(r#"respCountries: ["ZZ", "ZZ"]"#), + "data: {data}" + ); + } + + #[tokio::test] + async fn multi_raw_event_types_expose_precise_time_fields() { + let schema = TestSchema::new().await; + let type_names = [ + "PortScan", + "MultiHostPortScan", + "ExternalDdos", + "FtpBruteForce", + "LdapBruteForce", + "RdpBruteForce", + "RepeatedHttpSessions", + ]; + let selections = type_names + .iter() + .enumerate() + .map(|(index, type_name)| { + format!( + "eventType{index}: __type(name: \"{type_name}\") {{ fields {{ name description }} }}" + ) + }) + .collect::>() + .join(" "); + let res = schema + .execute_as_system_admin(&format!("{{ {selections} }}")) + .await; + assert!(res.errors.is_empty(), "errors: {:?}", res.errors); + let data = res.data.into_json().unwrap(); + + for (index, type_name) in type_names.iter().enumerate() { + let fields = data[format!("eventType{index}")]["fields"] + .as_array() + .expect("fields array"); + let field = |name| fields.iter().find(|field| field["name"] == name); + assert_eq!( + field("firstEventStartTime").map(|value| &value["description"]), + Some(&serde_json::json!( + "Session start time of the first raw event used for detection." + )), + "type: {type_name}" + ); + assert_eq!( + field("lastEventStartTime").map(|value| &value["description"]), + Some(&serde_json::json!( + "Session start time of the last raw event used for detection." + )), + "type: {type_name}" + ); + assert!(field("startTime").is_none(), "type: {type_name}"); + assert!(field("endTime").is_none(), "type: {type_name}"); + } + } + + #[tokio::test] + async fn unusual_destination_pattern_exposes_sampling_window_times() { + let schema = TestSchema::new().await; + let res = schema + .execute_as_system_admin( + r#"{ __type(name: "UnusualDestinationPattern") { + fields { name description } + } }"#, + ) + .await; + assert!(res.errors.is_empty(), "errors: {:?}", res.errors); + let data = res.data.into_json().unwrap(); + let fields = data["__type"]["fields"].as_array().expect("fields array"); + let field = |name| fields.iter().find(|field| field["name"] == name); + assert_eq!( - res.data.to_string(), - r#"{event: {origCountry: "ZZ", respCountries: ["ZZ", "ZZ"]}}"# + field("samplingWindowStartTime").map(|value| &value["description"]), + Some(&serde_json::json!( + "Start boundary of the anomaly sampling window used for detection." + )) + ); + assert_eq!( + field("samplingWindowEndTime").map(|value| &value["description"]), + Some(&serde_json::json!( + "End boundary of the anomaly sampling window used for detection." + )) ); + assert!(field("startTime").is_none()); + assert!(field("endTime").is_none()); } #[tokio::test] @@ -4469,13 +4571,13 @@ mod tests { .unwrap() .and_local_timezone(Utc) .unwrap(); - let start_time = NaiveDate::from_ymd_opt(2018, 1, 26) + let sampling_window_start_time = NaiveDate::from_ymd_opt(2018, 1, 26) .unwrap() .and_hms_micro_opt(18, 0, 0, 0) .unwrap() .and_local_timezone(Utc) .unwrap(); - let end_time = NaiveDate::from_ymd_opt(2018, 1, 26) + let sampling_window_end_time = NaiveDate::from_ymd_opt(2018, 1, 26) .unwrap() .and_hms_micro_opt(18, 30, 0, 0) .unwrap() @@ -4484,8 +4586,8 @@ mod tests { let fields = UnusualDestinationPatternFields { sensor: "sensor1".to_string(), - start_time: start_time.timestamp_nanos_opt().unwrap(), - end_time: end_time.timestamp_nanos_opt().unwrap(), + sampling_window_start_time: sampling_window_start_time.timestamp_nanos_opt().unwrap(), + sampling_window_end_time: sampling_window_end_time.timestamp_nanos_opt().unwrap(), destination_ips: vec![ Ipv4Addr::new(192, 168, 1, 1).into(), Ipv4Addr::new(192, 168, 1, 2).into(), @@ -4513,6 +4615,8 @@ mod tests { node {{ \ ... on UnusualDestinationPattern {{ \ sensor \ + samplingWindowStartTime \ + samplingWindowEndTime \ respAddrs \ count \ expectedMean \ @@ -4531,6 +4635,20 @@ mod tests { let res = schema.execute_as_system_admin(&query).await; let data = res.data.to_string(); assert!(data.contains("sensor1")); + assert!( + data.contains(&format!( + "samplingWindowStartTime: \"{}\"", + sampling_window_start_time.to_rfc3339() + )), + "data: {data}" + ); + assert!( + data.contains(&format!( + "samplingWindowEndTime: \"{}\"", + sampling_window_end_time.to_rfc3339() + )), + "data: {data}" + ); assert!(data.contains("192.168.1.1")); assert!(data.contains("192.168.1.2")); assert!(data.contains("192.168.1.3")); diff --git a/src/graphql/event/conn.rs b/src/graphql/event/conn.rs index a8a99a68..c16dc2cf 100644 --- a/src/graphql/event/conn.rs +++ b/src/graphql/event/conn.rs @@ -96,14 +96,14 @@ impl PortScan { self.inner.proto } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Confidence @@ -237,14 +237,14 @@ impl MultiHostPortScan { self.inner.proto } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Confidence @@ -373,14 +373,14 @@ impl ExternalDdos { self.inner.proto } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Confidence diff --git a/src/graphql/event/ftp.rs b/src/graphql/event/ftp.rs index 329a813f..2e825d7c 100644 --- a/src/graphql/event/ftp.rs +++ b/src/graphql/event/ftp.rs @@ -161,14 +161,14 @@ impl FtpBruteForce { &self.inner.user_list } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Is Internal diff --git a/src/graphql/event/http.rs b/src/graphql/event/http.rs index 188eb8cf..7934560d 100644 --- a/src/graphql/event/http.rs +++ b/src/graphql/event/http.rs @@ -376,14 +376,14 @@ impl RepeatedHttpSessions { self.inner.proto } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Responder Country diff --git a/src/graphql/event/ldap.rs b/src/graphql/event/ldap.rs index f3c8adcb..2239b8d9 100644 --- a/src/graphql/event/ldap.rs +++ b/src/graphql/event/ldap.rs @@ -101,14 +101,14 @@ impl LdapBruteForce { .collect() } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Confidence diff --git a/src/graphql/event/rdp.rs b/src/graphql/event/rdp.rs index 2b2944ea..5f9d546e 100644 --- a/src/graphql/event/rdp.rs +++ b/src/graphql/event/rdp.rs @@ -90,14 +90,14 @@ impl RdpBruteForce { self.inner.proto } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Session start time of the first raw event used for detection. + async fn first_event_start_time(&self) -> DateTime { + self.inner.first_event_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// Session start time of the last raw event used for detection. + async fn last_event_start_time(&self) -> DateTime { + self.inner.last_event_start_time } /// Confidence diff --git a/src/graphql/event/unusual_destination_pattern.rs b/src/graphql/event/unusual_destination_pattern.rs index 9a2f3faa..c9b2d6c7 100644 --- a/src/graphql/event/unusual_destination_pattern.rs +++ b/src/graphql/event/unusual_destination_pattern.rs @@ -31,14 +31,14 @@ impl UnusualDestinationPattern { &self.inner.sensor } - /// Detection Start Time - async fn start_time(&self) -> DateTime { - self.inner.start_time + /// Start boundary of the anomaly sampling window used for detection. + async fn sampling_window_start_time(&self) -> DateTime { + self.inner.sampling_window_start_time } - /// Detection End Time - async fn end_time(&self) -> DateTime { - self.inner.end_time + /// End boundary of the anomaly sampling window used for detection. + async fn sampling_window_end_time(&self) -> DateTime { + self.inner.sampling_window_end_time } /// Responder IP (Address) List From dc4cedabc9fcbab5104c20bbe1dbc6c850022ebf Mon Sep 17 00:00:00 2001 From: Hanbeom kim Date: Fri, 21 Aug 2026 14:03:53 +0900 Subject: [PATCH 2/2] Restore CHANGELOG reference to bd30664 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a66fcd65..1134c7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Changed -- Bumped `review-database` dependency to commit `644db00`, which renames +- Bumped `review-database` dependency to commit `bd30664`, which renames several fields used internally by this crate: `Agent.node` and `ExternalService.node` are now `node_id`, `TrafficFilter.agent` is now `host_fqdn`, and `BlocklistKerberos.client_name` / `service_name` are