From 2a5d7ef817a4e5d00bf451ada2fce413a1338b75 Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Wed, 23 Sep 2026 11:22:33 +0100 Subject: [PATCH 01/31] fix(server): read replay buses atomically under one lock Signed-off-by: Artem Lytvyn --- crates/openshell-server/src/grpc/sandbox.rs | 100 ++--------- crates/openshell-server/src/tracing_bus.rs | 178 ++++++++++++++------ 2 files changed, 145 insertions(+), 133 deletions(-) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 1428b766db..ec8853322d 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -96,19 +96,6 @@ const RESUME_SPACE_GONE: &str = "resume_after_cursor belongs to a cursor space t const RESUME_CURSOR_AHEAD: &str = "resume_after_cursor is ahead of every cursor this sandbox has \ issued. Restart the watch with an empty resume_after_cursor."; -/// Whether `sandbox_id`'s cursor space is still the one that issued `epoch`. -/// -/// A teardown retires the space and the next publish mints a replacement that -/// renumbers from 1, so a surviving epoch is the only proof that a seq validated -/// earlier still addresses the same numbering. Absent counts as changed: there -/// is nothing left for the cursor to point into. -fn cursor_space_is(state: &ServerState, sandbox_id: &str, epoch: uuid::Uuid) -> bool { - state - .tracing_log_bus - .cursor_space(sandbox_id) - .is_some_and(|space| space.epoch == epoch) -} - #[derive(Debug)] pub struct WatchSandboxStream { receiver: ReceiverStream>, @@ -1942,40 +1929,20 @@ pub(super) async fn handle_watch_sandbox( Some(_) => {} } - let log_replay = if follow_logs { - Some(state.tracing_log_bus.tail_after(&sandbox_id, resume.seq)) - } else { - None - }; - - let platform_replay = if follow_events { - Some( - state - .tracing_log_bus - .platform_event_bus - .tail_after(&sandbox_id, resume.seq), - ) - } else { - None - }; - - // Re-check the epoch now that both tails are in hand. The check - // above and each `tail_after` take their locks independently, so - // a teardown plus a republish can retire the validated space and - // install a replacement in between. The reads would then have - // applied the old space's seq to the new space's buffers, and - // `tail_after` -- which only knows numbers -- would report no gap - // while skipping every replacement event at or below it. The - // second look is cheap and runs before anything is emitted, so a - // space that moved under us ends the stream instead of serving a - // truncated replay. - // - // Ordering is unchanged: this takes only the allocator lock and - // releases it, never held across a bus lock. - if !cursor_space_is(&state, &sandbox_id, resume.epoch) { - let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; - return; - } + // Read both buses under one lock hold so a publish landing + // between them can't desync their high-water marks (see + // `TracingLogBus::snapshot_after`). This also closes the + // window the epoch re-check used to guard: a teardown plus + // republish can no longer land between the two reads, since + // there's only one, so revalidating the epoch afterward is no + // longer needed. + let (log_replay, platform_replay) = state.tracing_log_bus.snapshot_after( + &sandbox_id, + resume.seq, + resume.seq, + follow_logs, + follow_events, + ); // Gap check FIRST (borrows), before the merge moves the vecs. for replay in [&log_replay, &platform_replay] { @@ -4936,45 +4903,6 @@ mod tests { assert!(stream.next().await.is_none()); } - /// The guard behind the producer's post-replay epoch re-check. - /// - /// Validation and the two `tail_after` reads take their locks separately, so - /// a teardown plus a republish can swap the space in between and leave the - /// reads applying an old seq to a replacement's buffers -- `tail_after` only - /// compares numbers, so it reports no gap while skipping every replacement - /// event at or below that seq. The producer re-checks the epoch once both - /// tails are in hand and before emitting anything; this pins what that check - /// must answer. - /// - /// The interleaving itself is not reachable from a test: the producer runs - /// validation, both reads, and the re-check with no await in between, so - /// there is nothing to suspend it on. - #[tokio::test] - async fn cursor_space_is_rejects_a_replacement_space() { - let state = test_server_state().await; - let sandbox = test_sandbox("respace", Vec::new()); - state.store.put_message(&sandbox).await.unwrap(); - let id = sandbox.object_id().to_string(); - - seed_log_lines(&state, &id, 1); - let original = state.tracing_log_bus.cursor_space(&id).unwrap().epoch; - assert!(cursor_space_is(&state, &id, original)); - - // Teardown alone leaves no space to point into. - state.tracing_log_bus.remove(&id); - assert!(state.tracing_log_bus.cursor_space(&id).is_none()); - assert!(!cursor_space_is(&state, &id, original)); - - // The republish installs a replacement renumbered from 1. Its seqs - // overlap the retired space's, so only the epoch separates them. - seed_log_lines(&state, &id, 1); - let replacement = state.tracing_log_bus.cursor_space(&id).unwrap(); - assert_ne!(replacement.epoch, original); - assert_eq!(replacement.highest_seq, 1); - assert!(!cursor_space_is(&state, &id, original)); - assert!(cursor_space_is(&state, &id, replacement.epoch)); - } - #[tokio::test] async fn resume_from_reset_cursor_space_terminates_out_of_range() { use tokio_stream::StreamExt as _; diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index fea7abad5c..07fdf4e8d0 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -29,6 +29,10 @@ struct Inner { per_id: HashMap, } +/// Result of a resumable-source replay read: the events after the requested +/// cursor, or the gap that made them unreplayable. +pub(crate) type ReplayResult = Result, ResumeGap>; + /// A buffered or broadcast stream event paired with its raw sequence number. /// /// The wire `SandboxStreamEvent.cursor` is an opaque token; ordering decisions @@ -274,18 +278,6 @@ impl TracingLogBus { self.seq.space(sandbox_id) } - pub(crate) fn tail_after( - &self, - sandbox_id: &str, - after_seq: u64, - ) -> Result, ResumeGap> { - let inner = self.inner.lock().expect("tracing bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || Ok(Vec::new()), - |per| tail_after_impl(&per.tail, per.last_trimmed_seq, after_seq), - ) - } - /// Publish a log line from an external source (e.g., sandbox push). /// /// Injects the line into the same broadcast channel and tail buffer @@ -329,6 +321,45 @@ impl TracingLogBus { } } } + + /// Read both resumable buses for a sandbox under one lock hold. + /// + /// `tail_after` on each bus independently takes and releases its own + /// bus-map lock; a publish landing between two independent calls is + /// visible to one and not the other, desyncing their high-water marks + /// against a live stream that treats them as being read at the same + /// instant. Locking the shared cursor space first — the same lock every + /// publish path holds across its own tail insert — makes this read + /// atomic against any publish on either bus. + pub(crate) fn snapshot_after( + &self, + sandbox_id: &str, + log_after: u64, + platform_after: u64, + want_log: bool, + want_platform: bool, + ) -> (Option, Option) { + let _spaces = self.seq.lock(); + let log = want_log.then(|| { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || Ok(Vec::new()), + |per| tail_after_impl(&per.tail, per.last_trimmed_seq, log_after), + ) + }); + let platform = want_platform.then(|| { + let inner = self + .platform_event_bus + .inner + .lock() + .expect("platform event bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || Ok(Vec::new()), + |per| tail_after_impl(&per.tail, per.last_trimmed_seq, platform_after), + ) + }); + (log, platform) + } } #[derive(Debug, Clone)] @@ -493,6 +524,82 @@ mod tests { assert!(tail_after_impl(&tail, 0, 99).expect("ok").is_empty()); } + /// `snapshot_after` must take the same lock `publish` holds across its own + /// tail insert, so a publish can never land between the log read and the + /// platform read. Proving this directly (rather than racing threads and + /// hoping to catch a bad interleaving) is what makes this test reliable: + /// hold the lock ourselves and show `snapshot_after` cannot proceed until + /// it is released. + #[test] + fn snapshot_after_blocks_while_cursor_space_is_locked() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-lock"; + bus.publish_external(make_log_event(sandbox_id, "line")); + + let guard = bus.seq.lock(); + + let bus2 = bus.clone(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let handle = std::thread::spawn(move || { + bus2.snapshot_after(sandbox_id, 0, 0, true, true); + done_tx.send(()).unwrap(); + }); + + // Give the worker time to reach (and block on) the lock. + std::thread::sleep(std::time::Duration::from_millis(50)); + assert_eq!( + done_rx.try_recv(), + Err(std::sync::mpsc::TryRecvError::Empty), + "snapshot_after returned without waiting for the cursor-space lock" + ); + + drop(guard); + done_rx + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("snapshot_after should complete once the lock is released"); + handle.join().unwrap(); + } + + /// Baseline correctness: interleaved log/platform publishes draw from the + /// same cursor space, and `snapshot_after` returns each source's own + /// events under its own key, not merged or cross-contaminated. + #[test] + fn snapshot_after_matches_independent_tail_after_calls() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-snap"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish( + sandbox_id, + SandboxStreamEvent { + payload: None, + cursor: String::new(), + }, + ); // seq 2 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 3 + + let (log, platform) = bus.snapshot_after(sandbox_id, 0, 0, true, true); + assert_eq!( + cursors(&log.expect("followed").expect("no gap")), + vec![1, 3] + ); + assert_eq!( + cursors(&platform.expect("followed").expect("no gap")), + vec![2] + ); + } + + /// A source that isn't followed isn't read at all. + #[test] + fn snapshot_after_skips_unfollowed_sources() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-skip"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + + let (log, platform) = bus.snapshot_after(sandbox_id, 0, 0, true, false); + assert_eq!(log.expect("followed").expect("no gap").len(), 1); + assert!(platform.is_none()); + } + #[test] fn tail_after_impl_boundary_at_last_trimmed_is_serviceable() { // Bus trimmed up to seq 2, retains 3..=5. Client saw exactly 2, so @@ -528,24 +635,27 @@ mod tests { } #[test] - fn tracing_log_bus_tail_after_serviceable_and_missing() { + fn tracing_log_bus_resume_replay_serviceable_and_missing() { let bus = TracingLogBus::new(); let sandbox_id = "sb-ta"; for _ in 0..3 { bus.publish_external(make_log_event(sandbox_id, "line")); } // Cursors start at 1, so three publishes are seqs 1,2,3. + let (log, _) = bus.snapshot_after(sandbox_id, 0, 0, true, false); assert_eq!( - cursors(&bus.tail_after(sandbox_id, 0).unwrap()), + cursors(&log.expect("followed").expect("no gap")), vec![1, 2, 3] ); - assert_eq!(cursors(&bus.tail_after(sandbox_id, 2).unwrap()), vec![3]); + let (log, _) = bus.snapshot_after(sandbox_id, 2, 0, true, false); + assert_eq!(cursors(&log.expect("followed").expect("no gap")), vec![3]); // Unknown sandbox: no entry, nothing buffered, no gap. - assert!(bus.tail_after("nope", 5).unwrap().is_empty()); + let (log, _) = bus.snapshot_after("nope", 5, 0, true, false); + assert!(log.expect("followed").expect("no gap").is_empty()); } #[test] - fn platform_event_bus_tail_after_serviceable() { + fn platform_event_bus_resume_replay_serviceable() { let bus = TracingLogBus::new(); let platform = &bus.platform_event_bus; let sandbox_id = "sb-pe"; @@ -554,32 +664,18 @@ mod tests { } // Shared allocator, but only the platform bus published here, so its // seqs are 1,2,3. + let (_, platform_replay) = bus.snapshot_after(sandbox_id, 0, 0, false, true); assert_eq!( - cursors(&platform.tail_after(sandbox_id, 0).unwrap()), + cursors(&platform_replay.expect("followed").expect("no gap")), vec![1, 2, 3] ); + let (_, platform_replay) = bus.snapshot_after(sandbox_id, 0, 1, false, true); assert_eq!( - cursors(&platform.tail_after(sandbox_id, 1).unwrap()), + cursors(&platform_replay.expect("followed").expect("no gap")), vec![2, 3] ); } - #[test] - fn shared_allocator_interleaves_cursors_across_buses() { - let bus = TracingLogBus::new(); - let sandbox_id = "sb-mix"; - // Interleave log and platform publishes; the shared allocator gives - // each a unique, increasing cursor in one merged space. - bus.publish_external(make_log_event(sandbox_id, "a")); // seq 1 - bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 - bus.publish_external(make_log_event(sandbox_id, "b")); // seq 3 - - let logs = cursors(&bus.tail_after(sandbox_id, 0).unwrap()); - let events = cursors(&bus.platform_event_bus.tail_after(sandbox_id, 0).unwrap()); - assert_eq!(logs, vec![1, 3]); - assert_eq!(events, vec![2]); - } - #[test] fn tracing_log_bus_remove_cleans_up_all_maps() { let bus = TracingLogBus::new(); @@ -971,18 +1067,6 @@ impl PlatformEventBus { .collect() } - pub(crate) fn tail_after( - &self, - sandbox_id: &str, - after_seq: u64, - ) -> Result, ResumeGap> { - let inner = self.inner.lock().expect("platform event bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || Ok(Vec::new()), - |per| tail_after_impl(&per.tail, per.last_trimmed_seq, after_seq), - ) - } - /// Remove the bus entry for the given sandbox id. /// /// This drops the broadcast sender, closing any active receivers, From c28356dd26749e2d6be43e107487ffd49d406da6 Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Wed, 23 Sep 2026 11:57:16 +0100 Subject: [PATCH 02/31] fix(server): clamp replay to cross-source coverage floor Signed-off-by: Artem Lytvyn --- crates/openshell-server/src/grpc/sandbox.rs | 184 ++++++++++++++++++-- crates/openshell-server/src/tracing_bus.rs | 158 +++++++++++++++-- 2 files changed, 309 insertions(+), 33 deletions(-) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index ec8853322d..65aa247cad 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -2016,26 +2016,86 @@ pub(super) async fn handle_watch_sandbox( // cursors expects. // // The two windows are truncated independently (log_tail vs - // event_tail), so this merges whatever each bus retained; it - // does not align their depths. - let mut tail: Vec = Vec::new(); - if follow_logs { - let logs = state.tracing_log_bus.tail(&sandbox_id, log_tail as usize); - if let Some(last) = logs.last() { - log_cutoff = log_cutoff.max(last.seq); - } - tail.extend(logs); - } - if follow_events { - let events = state + // event_tail). A client tracks one scalar cursor across both + // sources -- whatever the highest one it saw was -- so a + // cursor this batch hands out is only safe to resume from if + // *every* followed source can vouch it covered everything up + // to that point. + // + // `tail_with_floor` reports each source's own coverage floor: + // the newest event its window excluded, i.e. the point below + // which that source cannot prove nothing was skipped. The + // nearest such point across both followed sources bounds what + // this whole batch may hand out -- not only the deeper + // source's excess over the shallower one. A log event whose + // seq clears the log bus's own floor can still be at or past + // the *platform* bus's floor; delivering it would still let + // the client's remembered cursor exceed platform's unreplayed + // backlog, and a later resume would skip that backlog with no + // gap reported, since nothing was evicted, just never sent. + // + // This can mean far fewer events than `log_tail_lines` / + // `event_tail` asked for -- even zero -- whenever a followed + // sibling has any unreplayed backlog (e.g. `event_tail` + // unset, its default, on a sandbox with platform history). + // That's the deliberate trade-off of a single shared scalar + // cursor: silent loss is worse than an emptier initial batch. + let mut logs = Vec::new(); + let log_floor = if follow_logs { + let (l, floor) = state + .tracing_log_bus + .tail_with_floor(&sandbox_id, log_tail as usize); + logs = l; + floor + } else { + 0_u64 + }; + + let mut events = Vec::new(); + let platform_floor = if follow_events { + let (e, floor) = state .tracing_log_bus .platform_event_bus - .tail(&sandbox_id, event_tail as usize); - if let Some(last) = events.last() { - platform_cutoff = platform_cutoff.max(last.seq); - } - tail.extend(events); + .tail_with_floor(&sandbox_id, event_tail as usize); + events = e; + floor + } else { + 0_u64 + }; + + // 0 means "nothing excluded" for a source, not "excluded up + // to 0" -- exclude it from the minimum so a fully-covered + // source never becomes the binding constraint. + let critical_floor = [ + follow_logs.then_some(log_floor), + follow_events.then_some(platform_floor), + ] + .into_iter() + .flatten() + .filter(|&floor| floor > 0) + .min(); + + if let Some(cap) = critical_floor { + logs.retain(|cursored| cursored.seq < cap); + events.retain(|cursored| cursored.seq < cap); + } + + // Cutoffs reflect only what this batch actually delivered. + // Withheld backlog was already published before this + // connect, so it can never arrive again via the live + // broadcast -- there's nothing to suppress a duplicate of, + // and raising a cutoff past what was sent would risk + // swallowing a genuinely new live event with a seq in the + // withheld range instead. + let mut tail: Vec = Vec::new(); + if let Some(last) = logs.last() { + log_cutoff = log_cutoff.max(last.seq); + } + tail.extend(logs); + if let Some(last) = events.last() { + platform_cutoff = platform_cutoff.max(last.seq); } + tail.extend(events); tail.sort_by_key(|cursored| cursored.seq); @@ -4553,6 +4613,96 @@ mod tests { assert_eq!(got, vec![1, 2, 3, 4]); } + /// The A2 repro: a platform event exists (seq 1) that `event_tail: 0` + /// deliberately withholds from the initial batch, while `follow_logs` + /// delivers two later logs (seq 2, 3). Without the cross-source + /// coverage floor, the client would see cursors 2 and 3, remember 3 as + /// its high-water mark, and a future resume from 3 would exclude the + /// platform event forever -- `tail_after` reports no gap, since nothing + /// was evicted, it was just never sent. The floor must instead withhold + /// the logs too, since platform's own floor (1) is the nearest point + /// neither source can vouch past. + #[tokio::test] + async fn initial_tail_withholds_logs_behind_a_sibling_sources_unreplayed_backlog() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1, never replayed (event_tail: 0) + seed_log_lines(&state, &id, 2); // cursors 2, 3 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + // Default: no backlog requested from the platform bus. + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + // Platform's floor (1) is the binding constraint: nothing at or + // above it may be handed out, so both logs are withheld even though + // `log_tail_lines` alone would have covered them. + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!( + next.is_err(), + "expected the coverage floor to withhold every buffered event, got {next:?}" + ); + } + + /// Symmetric depths (or a source that's fully covered) impose no floor: + /// this is the common case and must behave exactly as before A2. + #[tokio::test] + async fn initial_tail_unclamped_when_no_source_has_unreplayed_backlog() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("nofloor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1 + seed_log_lines(&state, &id, 2); // cursors 2, 3 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + // Deep enough to cover the one platform event: no floor. + event_tail: 10, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut got = Vec::new(); + for _ in 0..3 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![1, 2, 3]); + } + #[tokio::test] async fn live_delivery_orders_events_across_sources_by_cursor() { use tokio_stream::StreamExt as _; diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index 07fdf4e8d0..f5e70cda52 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -186,6 +186,31 @@ fn tail_after_impl( Ok(res) } +/// Split a tail into what a bounded request returns and the coverage floor +/// that bound leaves behind. +/// +/// `max` may return fewer events than the tail currently retains; the ones +/// excluded are the oldest, at the front. The floor is the newest excluded +/// event's seq -- the point below which this call cannot vouch that nothing +/// was skipped, because it was skipped on request, not on eviction. `0` +/// means `max` covered everything currently retained, so there's nothing +/// this call withheld. +/// +/// This is what lets a caller answer "is a cursor from this batch safe to +/// resume from," which `tail_after`'s eviction-only gap check can't: a +/// shallow request and a bus eviction both withhold events, but only the +/// eviction leaves a trace in `last_trimmed_seq`. +fn tail_with_floor_impl(tail: &VecDeque, max: usize) -> (Vec, u64) { + let total = tail.len(); + let events: Vec = tail.iter().rev().take(max).cloned().collect(); + let floor = if events.len() >= total { + 0 + } else { + tail[total - events.len() - 1].seq + }; + (events.into_iter().rev().collect(), floor) +} + impl Default for TracingLogBus { fn default() -> Self { Self::new() @@ -269,6 +294,24 @@ impl TracingLogBus { .collect::>() } + /// Like `tail`, but also reports the coverage floor `max` leaves behind. + /// + /// See `tail_with_floor_impl`: a cursor at or below the returned floor + /// isn't safe to advertise as a resume point, because `max` -- not + /// eviction -- is why anything at or below it is missing from this + /// batch. + pub(crate) fn tail_with_floor( + &self, + sandbox_id: &str, + max: usize, + ) -> (Vec, u64) { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || (Vec::new(), 0), + |per| tail_with_floor_impl(&per.tail, max), + ) + } + /// Identity and extent of this sandbox's current cursor space. /// /// `None` means nothing has been published for the sandbox, so there is no @@ -524,6 +567,47 @@ mod tests { assert!(tail_after_impl(&tail, 0, 99).expect("ok").is_empty()); } + #[test] + fn tail_with_floor_impl_empty_tail_returns_zero_floor() { + let tail = VecDeque::new(); + let (events, floor) = tail_with_floor_impl(&tail, 10); + assert!(events.is_empty()); + assert_eq!(floor, 0); + } + + #[test] + fn tail_with_floor_impl_max_covers_everything_returns_zero_floor() { + let tail = tail_of(1, 5); + let (events, floor) = tail_with_floor_impl(&tail, 5); + assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); + assert_eq!(floor, 0); + + // Asking for more than exists is the same as asking for everything. + let (events, floor) = tail_with_floor_impl(&tail, 99); + assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); + assert_eq!(floor, 0); + } + + #[test] + fn tail_with_floor_impl_truncated_reports_newest_excluded_seq() { + let tail = tail_of(1, 5); + // Newest 2 returned (4, 5); 1..=3 excluded, newest of those is 3. + let (events, floor) = tail_with_floor_impl(&tail, 2); + assert_eq!(cursors(&events), vec![4, 5]); + assert_eq!(floor, 3); + } + + #[test] + fn tail_with_floor_impl_zero_max_excludes_everything() { + let tail = tail_of(1, 5); + // Nothing returned; the floor must cover every event that exists, + // not just the oldest one -- otherwise a sibling source could still + // treat seq 4 or 5 as safely skippable. + let (events, floor) = tail_with_floor_impl(&tail, 0); + assert!(events.is_empty()); + assert_eq!(floor, 5); + } + /// `snapshot_after` must take the same lock `publish` holds across its own /// tail insert, so a publish can never land between the log read and the /// platform read. Proving this directly (rather than racing threads and @@ -676,6 +760,42 @@ mod tests { ); } + #[test] + fn tracing_log_bus_tail_with_floor_reports_truncation() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-floor"; + for _ in 0..5 { + bus.publish_external(make_log_event(sandbox_id, "line")); + } + let (events, floor) = bus.tail_with_floor(sandbox_id, 2); + assert_eq!(cursors(&events), vec![4, 5]); + assert_eq!(floor, 3); + + // Wide enough to cover everything: no floor. + let (events, floor) = bus.tail_with_floor(sandbox_id, 10); + assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); + assert_eq!(floor, 0); + + // Unknown sandbox: nothing buffered, no floor. + let (events, floor) = bus.tail_with_floor("nope", 10); + assert!(events.is_empty()); + assert_eq!(floor, 0); + } + + #[test] + fn platform_event_bus_tail_with_floor_reports_truncation() { + let bus = TracingLogBus::new(); + let platform = &bus.platform_event_bus; + let sandbox_id = "sb-pe-floor"; + for _ in 0..5 { + platform.publish(sandbox_id, stream_event(0)); + } + let (events, floor) = platform.tail_with_floor(sandbox_id, 0); + assert!(events.is_empty()); + // Nothing returned: floor must cover every event published so far. + assert_eq!(floor, 5); + } + #[test] fn tracing_log_bus_remove_cleans_up_all_maps() { let bus = TracingLogBus::new(); @@ -757,7 +877,9 @@ mod tests { assert_eq!(after_platform.highest_seq, 2); let log_cursor = &bus.tail(sandbox_id, 10)[0].event.cursor; - let platform_cursor = &bus.platform_event_bus.tail(sandbox_id, 10)[0].event.cursor; + let platform_cursor = &bus.platform_event_bus.tail_with_floor(sandbox_id, 10).0[0] + .event + .cursor; assert_eq!( WatchCursor::parse(log_cursor).expect("valid").epoch, WatchCursor::parse(platform_cursor).expect("valid").epoch, @@ -944,8 +1066,9 @@ mod tests { } // Tail should return all events in order - let events = bus.tail(sandbox_id, 10); + let (events, floor) = bus.tail_with_floor(sandbox_id, 10); assert_eq!(events.len(), 5); + assert_eq!(floor, 0, "max covered everything retained"); // Verify order (oldest first) for (i, cursored) in events.iter().enumerate() { @@ -957,8 +1080,9 @@ mod tests { } // Tail with smaller max should return most recent events - let events = bus.tail(sandbox_id, 2); + let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(events.len(), 2); + assert_eq!(floor, 3, "newest excluded event is Event2 (seq 3)"); if let Some(sandbox_stream_event::Payload::Event(ref e)) = events[0].event.payload { assert_eq!(e.reason, "Event3"); } @@ -970,8 +1094,9 @@ mod tests { #[test] fn platform_event_bus_tail_empty_sandbox() { let bus = PlatformEventBus::new(SeqAllocator::default()); - let events = bus.tail("nonexistent", 10); + let (events, floor) = bus.tail_with_floor("nonexistent", 10); assert!(events.is_empty()); + assert_eq!(floor, 0); } #[test] @@ -984,10 +1109,10 @@ mod tests { cursor: String::new(), }; bus.publish(sandbox_id, evt); - assert_eq!(bus.tail(sandbox_id, 10).len(), 1); + assert_eq!(bus.tail_with_floor(sandbox_id, 10).0.len(), 1); bus.remove(sandbox_id); - assert!(bus.tail(sandbox_id, 10).is_empty()); + assert!(bus.tail_with_floor(sandbox_id, 10).0.is_empty()); } } @@ -1054,17 +1179,18 @@ impl PlatformEventBus { } } - /// Return buffered platform events for replay to late subscribers. - pub(crate) fn tail(&self, sandbox_id: &str, max: usize) -> Vec { + /// Return buffered platform events and the coverage floor `max` leaves + /// behind. See `TracingLogBus::tail_with_floor`. + pub(crate) fn tail_with_floor( + &self, + sandbox_id: &str, + max: usize, + ) -> (Vec, u64) { let inner = self.inner.lock().expect("platform event bus lock poisoned"); - inner - .per_id - .get(sandbox_id) - .map(|d| d.tail.iter().rev().take(max).cloned().collect::>()) - .unwrap_or_default() - .into_iter() - .rev() - .collect() + inner.per_id.get(sandbox_id).map_or_else( + || (Vec::new(), 0), + |per| tail_with_floor_impl(&per.tail, max), + ) } /// Remove the bus entry for the given sandbox id. From 8ec87ac1f5019dcdfac5d60c9ebefecfda03924e Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Wed, 23 Sep 2026 12:07:44 +0100 Subject: [PATCH 03/31] docs: document cross-source coverage floor for WatchSandbox resume Signed-off-by: Artem Lytvyn --- architecture/gateway.md | 59 +++++++++++++++++++-------- docs/observability/accessing-logs.mdx | 2 + proto/openshell.proto | 20 +++++++++ 3 files changed, 64 insertions(+), 17 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index e065fd8e1a..467a73c172 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -516,23 +516,48 @@ whatever each bus kept; it does not align their depths. The broadcast receivers are subscribed before replay, so an event buffered during initialization could appear in both replay and the live receiver; the producer tracks the highest replayed seq and suppresses live events at or below it, so -each event is delivered once. That mark is per source. The two tails are read at -different instants and bounded independently, so one shared mark would let the -deeper source censor the shallower one — with `event_tail` unset the mark rises -to the newest buffered log while no platform event is replayed at all, and -platform events published during initialization are discarded as duplicates of a -replay that never ran. Subscribing never mints a cursor space, so a -resume against a torn-down sandbox cannot create the space its stale cursor is -then checked against. Clients track the highest observed `cursor` and pass it as -`resume_after_cursor` on reconnect. - -The epoch is validated twice on resume: once before reading the tails and again -once both are in hand, before anything is emitted. The check and each read take -their locks separately, so a teardown plus a republish can retire the validated -space and install a replacement in between; the reads would then apply the old -space's seq to the replacement's buffers, and a trimmed-range check that only -compares numbers would report no gap while skipping the replacement's lower -events. The second look ends the stream with `OUT_OF_RANGE` instead. +each event is delivered once. That mark is per source, because the two tails are +read at different instants and bounded independently — one shared mark would let +the deeper source censor the shallower one, discarding the shallower source's own +live arrivals as duplicates of a replay that never ran. Subscribing never mints a +cursor space, so a resume against a torn-down sandbox cannot create the space its +stale cursor is then checked against. Clients track the highest observed `cursor` +and pass it as `resume_after_cursor` on reconnect. + +On resume, both bus reads happen under one lock — `TracingLogBus::snapshot_after` +— rather than as two independent calls. A publish landing between two +independently-locked reads would be visible to whichever ran second and not the +other, desyncing their high-water marks against a live stream that treats them as +read at the same instant; holding the cursor-space lock (the same one every +publish holds across its own tail insert) across both reads closes that window. +The epoch is validated once before the reads, under this scheme — a second +post-read check isn't needed once there's no window left for a teardown and +republish to land in. + +#### Coverage floor + +A cursor is one opaque scalar shared by both sources; a client tracks its single +highest observed value, not one per source. That's only safe if, for whatever +cursor a batch hands out, *every* followed source can vouch it covered +everything up to that point — otherwise a later resume replays each bus +unconditionally past the cursor (`tail_after` isn't bounded by `log_tail_lines` +or `event_tail`), and anything a shallower source withheld below that cursor is +excluded forever with no gap reported, since it was never evicted, just never +sent. + +`log_tail_lines` and `event_tail` bound the two tails independently, so their +depths are routinely asymmetric — `event_tail` has no default, so `follow_events` +without setting it replays no platform backlog at all. On connect, the server +computes each followed source's **coverage floor** — the newest event that +source's own window excluded — and withholds every event, from either source, at +or above the smallest nonzero floor. A log event that clears the log bus's own +floor can still sit at or past the platform bus's floor; handing it out would +still let the client's cursor outrun platform's unreplayed backlog. + +This can withhold far more than either depth parameter alone implies — even the +entire batch — whenever a followed sibling has any backlog the request didn't +ask to replay. That is a deliberate trade-off: an emptier initial batch is +preferable to a resume that silently and permanently drops events. ## Persistence diff --git a/docs/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx index e5981c9583..aa09595be2 100644 --- a/docs/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -58,6 +58,8 @@ On reconnect, a client passes the highest cursor it processed as the resume poin The gateway merges the log and platform event sources before emitting, so events normally arrive in ascending cursor order. That applies to the buffered tail you receive when the stream opens, to a replay after a resume, and to live delivery. The gateway does not delay an event to wait for a lower cursor that has not been published yet, so a cursor can still arrive late under concurrent publication. Track the highest cursor seen as the resume point rather than the last one received. +A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events through the SDK and set how far back each replays independently, an asymmetric depth can reduce how much either source delivers on connect — the gateway withholds events rather than hand out a cursor that one of the two sources cannot fully vouch for. `openshell logs` doesn't expose this dial and isn't affected; it applies to SDK callers who set both replay depths directly. + ## Direct Filesystem Access Start an independent shell with `sandbox exec` to read log files directly: diff --git a/proto/openshell.proto b/proto/openshell.proto index ed18c86b25..363941a78a 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1998,9 +1998,23 @@ message WatchSandboxRequest { bool follow_events = 4; // Replay the last N log lines (best-effort) before following. + // + // When both follow_logs and follow_events are set, a cursor handed back on + // this connect is only safe to resume from if *every* followed source can + // vouch it covers everything up to that point (see resume_after_cursor). + // If this depth and event_tail are asymmetric -- e.g. event_tail is left + // at its default of 0 while this sandbox has platform history -- the + // server withholds events from whichever source that would otherwise let + // a resumable cursor outrun the other source's replay, even below this + // limit. Symmetric depths, or a sandbox with no backlog on the shallower + // source, are unaffected. uint32 log_tail_lines = 5; // Replay the last N platform events (best-effort) before following. + // + // Has no default: 0 replays no platform events on connect. See + // log_tail_lines for how an asymmetric depth against that field can + // reduce what either source delivers on this connect. uint32 event_tail = 6; // Stop streaming once the sandbox reaches READY or a terminal result phase @@ -2034,6 +2048,12 @@ message WatchSandboxRequest { // empty resume_after_cursor, because retrying the same token fails // identically. A cursor this server could not have issued is rejected with // INVALID_ARGUMENT. + // + // The cursor is a single opaque position shared across log and platform + // events; the client tracks one scalar, not one per source. When + // follow_logs and follow_events are both set, the server never hands out a + // cursor that one of the two sources cannot fully vouch for -- see + // log_tail_lines. string resume_after_cursor = 12; } From ffb3e8ac093b9f163a63c2663ad4150f9ed591db Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Wed, 23 Sep 2026 14:18:03 +0100 Subject: [PATCH 04/31] fix(server): validate resume cursor under the snapshot_after lock hold Signed-off-by: Artem Lytvyn --- architecture/gateway.md | 33 ++-- crates/openshell-server/src/grpc/sandbox.rs | 150 +++++++++++++++---- crates/openshell-server/src/sandbox_watch.rs | 66 ++++++++ crates/openshell-server/src/tracing_bus.rs | 147 +++++++++++++++--- docs/observability/accessing-logs.mdx | 2 +- 5 files changed, 335 insertions(+), 63 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index 467a73c172..e5942bbdaf 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -524,15 +524,17 @@ cursor space, so a resume against a torn-down sandbox cannot create the space it stale cursor is then checked against. Clients track the highest observed `cursor` and pass it as `resume_after_cursor` on reconnect. -On resume, both bus reads happen under one lock — `TracingLogBus::snapshot_after` -— rather than as two independent calls. A publish landing between two -independently-locked reads would be visible to whichever ran second and not the -other, desyncing their high-water marks against a live stream that treats them as -read at the same instant; holding the cursor-space lock (the same one every -publish holds across its own tail insert) across both reads closes that window. -The epoch is validated once before the reads, under this scheme — a second -post-read check isn't needed once there's no window left for a teardown and -republish to land in. +On resume, epoch validation and both bus reads happen inside one call under one +lock — `TracingLogBus::snapshot_after` — rather than as a validate-then-read +pair. A publish landing between two independently-locked reads would be visible +to whichever ran second and not the other, desyncing their high-water marks +against a live stream that treats them as read at the same instant; a teardown +plus a republish landing between a *separate* validation and read would apply +the old space's seq to the replacement's buffers and find no gap, since +`tail_after` only compares numbers, not epochs. Folding validation into the same +lock hold as the reads closes both windows at once: nothing can retire or +recreate the space while it's held, so a second post-read epoch check isn't +needed. #### Coverage floor @@ -559,6 +561,19 @@ entire batch — whenever a followed sibling has any backlog the request didn't ask to replay. That is a deliberate trade-off: an emptier initial batch is preferable to a resume that silently and permanently drops events. +Withholding alone would just move the silent loss from resume time to a live +event delivered moments later: nothing raises `log_cutoff`/`platform_cutoff` +past what this batch actually sent, so the very next live event past the floor +would still be delivered and would still let the client's cursor outrun the +withheld backlog. Refusing that delivery too doesn't help either — the withheld +events were published before subscribe and will never arrive live to fill the +gap in, so gating every later event on them would starve the stream +indefinitely instead of surfacing one bounded gap. The producer instead emits a +coverage-gap warning ahead of the batch, naming how many events were withheld +per source, and then proceeds with ordinary cutoffs. The gap is disclosed once, +up front, rather than silently, which is the bar the whole loss-awareness design +holds to — not that nothing is ever lost. + ## Persistence The gateway persistence layer is a protobuf object store. Domain services store diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 65aa247cad..6f824cb77a 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -18,7 +18,7 @@ use crate::pagination::Pagination; use crate::persistence::{ ObjectLabels, ObjectListQuery, ObjectType, WriteCondition, generate_name, }; -use crate::tracing_bus::CursoredEvent; +use crate::tracing_bus::{CursoredEvent, ResumeSnapshot}; use crate::watch_cursor::WatchCursor; use futures::future; use openshell_core::net::set_tcp_nodelay_best_effort; @@ -1908,41 +1908,38 @@ pub(super) async fn handle_watch_sandbox( // and silently swallow every live event beneath it. Comparing // epochs answers "did this cursor come from *this* space?", // which no numeric bound can. - match state.tracing_log_bus.cursor_space(&sandbox_id) { - None => { - let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; - return; - } - Some(space) if space.epoch != resume.epoch => { + // + // Validation and both bus reads happen inside one call, under + // one lock hold (see `TracingLogBus::snapshot_after`). A + // separate validate-then-read pair -- even with the two reads + // themselves atomic against each other -- still leaves a + // window between the validation's lock and the reads' lock + // for a teardown plus a republish to retire the validated + // space and install a replacement in between; the reads would + // then apply the old space's seq to the replacement's + // buffers and find no gap, since `tail_after` only compares + // numbers. Folding validation into the same hold as the reads + // closes that window entirely. + let (log_replay, platform_replay) = match state.tracing_log_bus.snapshot_after( + &sandbox_id, + resume.epoch, + resume.seq, + resume.seq, + follow_logs, + follow_events, + ) { + ResumeSnapshot::SpaceGone => { let _ = tx.send(Err(Status::out_of_range(RESUME_SPACE_GONE))).await; return; } - // Right space, but ahead of anything it issued: only a - // fabricated token gets here. Reject rather than accept a - // cutoff no event can ever exceed. - Some(space) if resume.seq > space.highest_seq => { + ResumeSnapshot::CursorAhead => { let _ = tx .send(Err(Status::out_of_range(RESUME_CURSOR_AHEAD))) .await; return; } - Some(_) => {} - } - - // Read both buses under one lock hold so a publish landing - // between them can't desync their high-water marks (see - // `TracingLogBus::snapshot_after`). This also closes the - // window the epoch re-check used to guard: a teardown plus - // republish can no longer land between the two reads, since - // there's only one, so revalidating the epoch afterward is no - // longer needed. - let (log_replay, platform_replay) = state.tracing_log_bus.snapshot_after( - &sandbox_id, - resume.seq, - resume.seq, - follow_logs, - follow_events, - ); + ResumeSnapshot::Read(log, platform) => (log, platform), + }; // Gap check FIRST (borrows), before the merge moves the vecs. for replay in [&log_replay, &platform_replay] { @@ -2075,10 +2072,15 @@ pub(super) async fn handle_watch_sandbox( .filter(|&floor| floor > 0) .min(); - if let Some(cap) = critical_floor { + let mut log_withheld = 0; + let platform_withheld = critical_floor.map_or(0, |cap| { + let before = logs.len(); logs.retain(|cursored| cursored.seq < cap); + log_withheld = before - logs.len(); + let before = events.len(); events.retain(|cursored| cursored.seq < cap); - } + before - events.len() + }); // Cutoffs reflect only what this batch actually delivered. // Withheld backlog was already published before this @@ -2099,6 +2101,26 @@ pub(super) async fn handle_watch_sandbox( tail.sort_by_key(|cursored| cursored.seq); + // Disclose the gap before anything else in this batch. The + // cutoffs above are never raised past what was actually + // delivered, so a later live event past the floor still + // reaches the client -- withholding it too would starve the + // stream indefinitely, since the backlog behind the floor + // was published before subscribe and will never arrive live + // to fill it in. The client has to learn the gap exists from + // this warning; a resume from any cursor at or above the + // floor cannot recover it, and `tail_after` reports no gap + // when asked, since nothing was evicted, only never sent. + if log_withheld > 0 || platform_withheld > 0 { + let warning = crate::sandbox_watch::coverage_gap_warning_event( + log_withheld, + platform_withheld, + ); + if tx.send(Ok(warning)).await.is_err() { + return; + } + } + for cursored in tail { // Log filters; platform events carry no log fields and pass. if let Some(openshell_core::proto::sandbox_stream_event::Payload::Log( @@ -4655,14 +4677,78 @@ mod tests { // Platform's floor (1) is the binding constraint: nothing at or // above it may be handed out, so both logs are withheld even though - // `log_tail_lines` alone would have covered them. + // `log_tail_lines` alone would have covered them. The client learns + // about it from an explicit warning rather than discovering it + // silently on a later reconnect. + let warning = stream.next().await.unwrap().unwrap(); + match warning.payload { + Some(openshell_core::proto::sandbox_stream_event::Payload::Warning(w)) => { + assert!(w.message.contains('2'), "message: {}", w.message); + } + other => panic!("expected a coverage-gap warning, got {other:?}"), + } + assert!(warning.cursor.is_empty()); + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; assert!( next.is_err(), - "expected the coverage floor to withhold every buffered event, got {next:?}" + "expected nothing further after the warning, got {next:?}" ); } + /// The coverage-gap warning discloses the withheld backlog once, up + /// front, rather than gating every later event: a live event published + /// after connect still reaches the client, even though its cursor sits + /// past the withheld platform backlog. The withheld events were + /// published before subscribe and can never arrive live, so refusing to + /// deliver anything past them would starve the stream indefinitely + /// instead of just disclosing the one, bounded gap. + #[tokio::test] + async fn live_delivery_proceeds_normally_after_the_coverage_gap_warning() { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floorlive", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1, withheld (event_tail: 0) + seed_log_lines(&state, &id, 1); // cursor 2, withheld too (below critical_floor) + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let warning = stream.next().await.unwrap().unwrap(); + assert!(matches!(warning.payload, Some(Payload::Warning(_)))); + + // Published after connect: a genuinely new live event, not part of + // the withheld backlog. It must still be delivered. + seed_log_lines(&state, &id, 1); // cursor 3, live + + let live = tokio::time::timeout(std::time::Duration::from_secs(2), stream.next()) + .await + .expect("live event should arrive") + .unwrap() + .unwrap(); + assert_eq!(seq_of(&live), 3); + } + /// Symmetric depths (or a source that's fully covered) impose no floor: /// this is the common case and must behave exactly as before A2. #[tokio::test] diff --git a/crates/openshell-server/src/sandbox_watch.rs b/crates/openshell-server/src/sandbox_watch.rs index 679912d352..7bc3ce8728 100644 --- a/crates/openshell-server/src/sandbox_watch.rs +++ b/crates/openshell-server/src/sandbox_watch.rs @@ -154,6 +154,43 @@ pub fn lag_warning_event(n: u64) -> openshell_core::proto::SandboxStreamEvent { } } +/// Build the warning payload emitted when the cross-source coverage floor +/// withholds events on connect. +/// +/// Unlike broadcast lag, nothing was evicted here: the withheld events are +/// still sitting in a bus's tail, excluded only because a sibling source's +/// shallower replay depth means handing them out would let the client's +/// single shared cursor imply coverage that sibling can't back. They won't +/// be resent later in this stream -- a resume replays past the cursor this +/// connect settles on, and these are at or below it by construction -- so +/// the client needs to know now, not discover it silently after a reconnect. +pub fn coverage_gap_warning(log_withheld: usize, platform_withheld: usize) -> SandboxStreamWarning { + SandboxStreamWarning { + message: format!( + "resume cursor cannot cover every followed source's backlog; withheld {log_withheld} \ + log line(s) and {platform_withheld} platform event(s) that a shallower sibling source \ + had not replayed. Restart the watch with a deeper log_tail_lines/event_tail if you need them; \ + they will not be recoverable from this cursor." + ), + } +} + +/// Wrap [`coverage_gap_warning`] in a `SandboxStreamEvent` ready to send. +pub fn coverage_gap_warning_event( + log_withheld: usize, + platform_withheld: usize, +) -> openshell_core::proto::SandboxStreamEvent { + use openshell_core::proto::sandbox_stream_event::Payload; + openshell_core::proto::SandboxStreamEvent { + payload: Some(Payload::Warning(coverage_gap_warning( + log_withheld, + platform_withheld, + ))), + // Warnings are not part of the resumable log/platform sequence. + cursor: String::new(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -226,6 +263,35 @@ mod tests { } } + #[test] + fn coverage_gap_warning_reports_withheld_counts() { + let warning = coverage_gap_warning(2, 1); + assert!( + warning.message.contains('2'), + "message: {}", + warning.message + ); + assert!( + warning.message.contains('1'), + "message: {}", + warning.message + ); + } + + #[test] + fn coverage_gap_warning_event_wraps_warning_payload_with_empty_cursor() { + use openshell_core::proto::sandbox_stream_event::Payload; + let evt = coverage_gap_warning_event(2, 1); + assert!(evt.cursor.is_empty()); + match evt.payload { + Some(Payload::Warning(w)) => { + assert!(w.message.contains('2')); + assert!(w.message.contains('1')); + } + other => panic!("expected Warning payload, got {other:?}"), + } + } + // Broadcast lag is recoverable at the tokio layer: after `Lagged`, the same // receiver keeps yielding the oldest surviving messages instead of closing. #[tokio::test] diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index f5e70cda52..039986b033 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -365,24 +365,44 @@ impl TracingLogBus { } } - /// Read both resumable buses for a sandbox under one lock hold. + /// Validate a resume cursor's epoch and read both resumable buses, all + /// under one lock hold. /// - /// `tail_after` on each bus independently takes and releases its own - /// bus-map lock; a publish landing between two independent calls is - /// visible to one and not the other, desyncing their high-water marks - /// against a live stream that treats them as being read at the same - /// instant. Locking the shared cursor space first — the same lock every - /// publish path holds across its own tail insert — makes this read - /// atomic against any publish on either bus. + /// Folding validation into the same hold as the reads is what makes this + /// atomic against a teardown: checking the epoch first and reading after + /// -- even via a single call that itself locks once internally -- still + /// leaves a window between the two lock acquisitions for a `remove` plus + /// a republish to retire the validated space and install a replacement + /// numbered from 1. A read landing after that would apply the old + /// space's seq to the replacement's buffers and find no gap, since + /// `tail_after` only compares numbers. Locking the cursor space before + /// checking the epoch -- the same lock every publish path holds across + /// its own tail insert -- closes that window entirely, and also makes + /// the two bus reads atomic against any publish on either bus, for the + /// same reason. pub(crate) fn snapshot_after( &self, sandbox_id: &str, + expected_epoch: Uuid, log_after: u64, platform_after: u64, want_log: bool, want_platform: bool, - ) -> (Option, Option) { - let _spaces = self.seq.lock(); + ) -> ResumeSnapshot { + let spaces = self.seq.lock(); + let Some(space) = spaces.get(sandbox_id) else { + return ResumeSnapshot::SpaceGone; + }; + if space.epoch != expected_epoch { + return ResumeSnapshot::SpaceGone; + } + // Right space, but ahead of anything it has issued: only a + // fabricated cursor gets here. Reject rather than accept a cutoff no + // event can ever exceed. + if log_after.max(platform_after) > space.next.saturating_sub(1) { + return ResumeSnapshot::CursorAhead; + } + let log = want_log.then(|| { let inner = self.inner.lock().expect("tracing bus lock poisoned"); inner.per_id.get(sandbox_id).map_or_else( @@ -401,10 +421,23 @@ impl TracingLogBus { |per| tail_after_impl(&per.tail, per.last_trimmed_seq, platform_after), ) }); - (log, platform) + ResumeSnapshot::Read(log, platform) } } +/// Outcome of [`TracingLogBus::snapshot_after`]. +pub(crate) enum ResumeSnapshot { + /// No cursor space exists for this sandbox, or its epoch no longer + /// matches the resume cursor's -- the space that issued it is gone. + SpaceGone, + /// The right space, but the cursor is ahead of anything it has issued. + /// Only a fabricated cursor reaches this. + CursorAhead, + /// Both requested reads succeeded at the lock hold; each is + /// independently either the replay or the gap that made it unreplayable. + Read(Option, Option), +} + #[derive(Debug, Clone)] struct SandboxLogLayer { bus: TracingLogBus, @@ -619,13 +652,14 @@ mod tests { let bus = TracingLogBus::new(); let sandbox_id = "sb-lock"; bus.publish_external(make_log_event(sandbox_id, "line")); + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; let guard = bus.seq.lock(); let bus2 = bus.clone(); let (done_tx, done_rx) = std::sync::mpsc::channel(); let handle = std::thread::spawn(move || { - bus2.snapshot_after(sandbox_id, 0, 0, true, true); + bus2.snapshot_after(sandbox_id, epoch, 0, 0, true, true); done_tx.send(()).unwrap(); }); @@ -660,8 +694,13 @@ mod tests { }, ); // seq 2 bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 3 + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; - let (log, platform) = bus.snapshot_after(sandbox_id, 0, 0, true, true); + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, true) + else { + panic!("expected a successful read"); + }; assert_eq!( cursors(&log.expect("followed").expect("no gap")), vec![1, 3] @@ -678,12 +717,63 @@ mod tests { let bus = TracingLogBus::new(); let sandbox_id = "sb-skip"; bus.publish_external(make_log_event(sandbox_id, "l1")); + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; - let (log, platform) = bus.snapshot_after(sandbox_id, 0, 0, true, false); + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, false) + else { + panic!("expected a successful read"); + }; assert_eq!(log.expect("followed").expect("no gap").len(), 1); assert!(platform.is_none()); } + /// The epoch check and both bus reads happen under one lock hold, so a + /// teardown plus republish between them can't apply a stale epoch's + /// cursor to the replacement space's buffers. + #[test] + fn snapshot_after_rejects_a_cursor_from_a_retired_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-retired"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + let original_epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + bus.remove(sandbox_id); + bus.publish_external(make_log_event(sandbox_id, "l1-again")); // seq 1 again, new epoch + + assert!(matches!( + bus.snapshot_after(sandbox_id, original_epoch, 0, 0, true, false), + ResumeSnapshot::SpaceGone + )); + } + + /// No space at all -- an unpublished or never-existent sandbox -- is the + /// same outcome as a retired one: there's nothing for the cursor to + /// address. + #[test] + fn snapshot_after_rejects_when_no_space_exists() { + let bus = TracingLogBus::new(); + assert!(matches!( + bus.snapshot_after("nope", Uuid::nil(), 0, 0, true, false), + ResumeSnapshot::SpaceGone + )); + } + + /// A cursor ahead of anything the space has issued -- only reachable with + /// a fabricated token -- is rejected rather than treated as caught up. + #[test] + fn snapshot_after_rejects_a_cursor_ahead_of_the_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-ahead"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + + assert!(matches!( + bus.snapshot_after(sandbox_id, epoch, 99, 0, true, false), + ResumeSnapshot::CursorAhead + )); + } + #[test] fn tail_after_impl_boundary_at_last_trimmed_is_serviceable() { // Bus trimmed up to seq 2, retains 3..=5. Client saw exactly 2, so @@ -725,17 +815,22 @@ mod tests { for _ in 0..3 { bus.publish_external(make_log_event(sandbox_id, "line")); } + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + // Cursors start at 1, so three publishes are seqs 1,2,3. - let (log, _) = bus.snapshot_after(sandbox_id, 0, 0, true, false); + let ResumeSnapshot::Read(log, _) = bus.snapshot_after(sandbox_id, epoch, 0, 0, true, false) + else { + panic!("expected a successful read"); + }; assert_eq!( cursors(&log.expect("followed").expect("no gap")), vec![1, 2, 3] ); - let (log, _) = bus.snapshot_after(sandbox_id, 2, 0, true, false); + let ResumeSnapshot::Read(log, _) = bus.snapshot_after(sandbox_id, epoch, 2, 0, true, false) + else { + panic!("expected a successful read"); + }; assert_eq!(cursors(&log.expect("followed").expect("no gap")), vec![3]); - // Unknown sandbox: no entry, nothing buffered, no gap. - let (log, _) = bus.snapshot_after("nope", 5, 0, true, false); - assert!(log.expect("followed").expect("no gap").is_empty()); } #[test] @@ -746,14 +841,24 @@ mod tests { for _ in 0..3 { platform.publish(sandbox_id, stream_event(0)); } + let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + // Shared allocator, but only the platform bus published here, so its // seqs are 1,2,3. - let (_, platform_replay) = bus.snapshot_after(sandbox_id, 0, 0, false, true); + let ResumeSnapshot::Read(_, platform_replay) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, false, true) + else { + panic!("expected a successful read"); + }; assert_eq!( cursors(&platform_replay.expect("followed").expect("no gap")), vec![1, 2, 3] ); - let (_, platform_replay) = bus.snapshot_after(sandbox_id, 0, 1, false, true); + let ResumeSnapshot::Read(_, platform_replay) = + bus.snapshot_after(sandbox_id, epoch, 0, 1, false, true) + else { + panic!("expected a successful read"); + }; assert_eq!( cursors(&platform_replay.expect("followed").expect("no gap")), vec![2, 3] diff --git a/docs/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx index aa09595be2..05b273264f 100644 --- a/docs/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -58,7 +58,7 @@ On reconnect, a client passes the highest cursor it processed as the resume poin The gateway merges the log and platform event sources before emitting, so events normally arrive in ascending cursor order. That applies to the buffered tail you receive when the stream opens, to a replay after a resume, and to live delivery. The gateway does not delay an event to wait for a lower cursor that has not been published yet, so a cursor can still arrive late under concurrent publication. Track the highest cursor seen as the resume point rather than the last one received. -A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events through the SDK and set how far back each replays independently, an asymmetric depth can reduce how much either source delivers on connect — the gateway withholds events rather than hand out a cursor that one of the two sources cannot fully vouch for. `openshell logs` doesn't expose this dial and isn't affected; it applies to SDK callers who set both replay depths directly. +A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events through the SDK and set how far back each replays independently, an asymmetric depth can reduce how much either source delivers on connect — the gateway withholds events rather than hand out a cursor that one of the two sources cannot fully vouch for, and reports what it withheld with a warning event rather than dropping it silently. `openshell logs` doesn't expose this dial and isn't affected; it applies to SDK callers who set both replay depths directly. ## Direct Filesystem Access From 9f4441cdbc8f8b86731a13bd39eeb55cee1b1ea5 Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Wed, 23 Sep 2026 15:05:47 +0100 Subject: [PATCH 05/31] fix(server): validate resume cursor under the snapshot_after lock hold Signed-off-by: Artem Lytvyn --- architecture/gateway.md | 14 ++-- crates/openshell-server/src/grpc/sandbox.rs | 82 ++++++++++++++++++--- crates/openshell-server/src/tracing_bus.rs | 45 ++++++----- 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index e5942bbdaf..e34f4e37d8 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -550,11 +550,15 @@ sent. `log_tail_lines` and `event_tail` bound the two tails independently, so their depths are routinely asymmetric — `event_tail` has no default, so `follow_events` without setting it replays no platform backlog at all. On connect, the server -computes each followed source's **coverage floor** — the newest event that -source's own window excluded — and withholds every event, from either source, at -or above the smallest nonzero floor. A log event that clears the log bus's own -floor can still sit at or past the platform bus's floor; handing it out would -still let the client's cursor outrun platform's unreplayed backlog. +computes each followed source's **coverage floor** — the *oldest* event that +source's own window excluded, i.e. the smallest seq it cannot vouch for — and +withholds every event, from either source, at or above the smallest nonzero +floor. It has to be the oldest, not the newest: a source's own excluded set is a +prefix of its own tail, but a sibling source's event can carry a seq strictly +between that source's oldest and newest excluded items. A boundary drawn at the +newest excluded item would let such a sibling event pass as safe, and handing it +out would still let the client's cursor outrun the source's older, still- +unreplayed backlog underneath it. This can withhold far more than either depth parameter alone implies — even the entire batch — whenever a followed sibling has any backlog the request didn't diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 6f824cb77a..b28c0fa83d 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -2020,16 +2020,20 @@ pub(super) async fn handle_watch_sandbox( // to that point. // // `tail_with_floor` reports each source's own coverage floor: - // the newest event its window excluded, i.e. the point below - // which that source cannot prove nothing was skipped. The - // nearest such point across both followed sources bounds what - // this whole batch may hand out -- not only the deeper - // source's excess over the shallower one. A log event whose - // seq clears the log bus's own floor can still be at or past - // the *platform* bus's floor; delivering it would still let - // the client's remembered cursor exceed platform's unreplayed - // backlog, and a later resume would skip that backlog with no - // gap reported, since nothing was evicted, just never sent. + // the *oldest* event its window excluded, i.e. the smallest + // seq that source cannot vouch for. It has to be the oldest, + // not the newest: a source's own excluded set is a prefix of + // its own tail, so a sibling source's event can carry a seq + // that falls strictly between this source's oldest and + // newest excluded items. A boundary at the newest excluded + // item would let that sibling event pass as safe, and + // delivering it would still let the client's remembered + // cursor reach past the excluded range -- a later resume + // would then skip the older withheld events in that range + // with no gap reported, since nothing was evicted, just + // never sent. The nearest such point across both followed + // sources bounds what this whole batch may hand out -- not + // only the deeper source's excess over the shallower one. // // This can mean far fewer events than `log_tail_lines` / // `event_tail` asked for -- even zero -- whenever a followed @@ -4749,6 +4753,64 @@ mod tests { assert_eq!(seq_of(&live), 3); } + /// Regression for the oldest-vs-newest-excluded floor bug. + /// + /// `event_tail: 0` excludes *both* of the platform bus's own events + /// (seqs 1 and 3); a log event (seq 2) sits interleaved between them and + /// is otherwise fully covered by `log_tail_lines`. A floor drawn at the + /// *newest* excluded platform seq (3) would let the log event (2 < 3) + /// pass as safe -- delivering it lets the client remember cursor 2, and + /// a resume from 2 would then permanently skip platform's seq 1, which + /// was never evicted and never sent. The floor must be the *oldest* + /// excluded seq (1) so the interleaved log event is withheld too. + #[tokio::test] + async fn initial_tail_withholds_an_interleaved_sibling_event_between_two_excluded_seqs() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("interleavedfloor", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1, withheld + seed_log_lines(&state, &id, 1); // cursor 2, must also be withheld + seed_platform_event(&state, &id, "e3"); // cursor 3, withheld + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let warning = stream.next().await.unwrap().unwrap(); + match warning.payload { + Some(openshell_core::proto::sandbox_stream_event::Payload::Warning(w)) => { + assert!(w.message.contains('1'), "message: {}", w.message); + } + other => panic!("expected a coverage-gap warning, got {other:?}"), + } + + // Nothing else: the log event must not slip through between + // platform's two excluded seqs. + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!( + next.is_err(), + "expected the interleaved log event to be withheld too, got {next:?}" + ); + } + /// Symmetric depths (or a source that's fully covered) impose no floor: /// this is the common case and must behave exactly as before A2. #[tokio::test] diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index 039986b033..ae51bd0947 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -190,11 +190,19 @@ fn tail_after_impl( /// that bound leaves behind. /// /// `max` may return fewer events than the tail currently retains; the ones -/// excluded are the oldest, at the front. The floor is the newest excluded -/// event's seq -- the point below which this call cannot vouch that nothing -/// was skipped, because it was skipped on request, not on eviction. `0` -/// means `max` covered everything currently retained, so there's nothing -/// this call withheld. +/// excluded are the oldest, at the front. The floor is the *oldest* excluded +/// event's seq -- the smallest seq this call cannot vouch for. `0` means +/// `max` covered everything currently retained, so there's nothing this call +/// withheld. +/// +/// It must be the oldest, not the newest, excluded seq. This bus's own +/// excluded set is a prefix of its own retained deque, so nothing above the +/// newest excluded item is ever missing *from this bus* -- but a sibling +/// bus's event can carry a seq that falls strictly between this bus's oldest +/// and newest excluded items, since the two buses interleave in one shared +/// seq space. A boundary drawn at the newest excluded item would let that +/// sibling event pass as safe, when delivering it would still let a client's +/// cursor reach past this bus's oldest withheld item on a later resume. /// /// This is what lets a caller answer "is a cursor from this batch safe to /// resume from," which `tail_after`'s eviction-only gap check can't: a @@ -206,7 +214,7 @@ fn tail_with_floor_impl(tail: &VecDeque, max: usize) -> (Vec= total { 0 } else { - tail[total - events.len() - 1].seq + tail.front().map_or(0, |cursored| cursored.seq) }; (events.into_iter().rev().collect(), floor) } @@ -622,23 +630,26 @@ mod tests { } #[test] - fn tail_with_floor_impl_truncated_reports_newest_excluded_seq() { + fn tail_with_floor_impl_truncated_reports_oldest_excluded_seq() { let tail = tail_of(1, 5); - // Newest 2 returned (4, 5); 1..=3 excluded, newest of those is 3. + // Newest 2 returned (4, 5); 1..=3 excluded, oldest of those is 1. + // Must be the oldest: a sibling source's event carrying seq 2 or 3 + // would otherwise pass a boundary drawn at 3 (the newest excluded) + // as safe, even though this bus hasn't vouched for anything below 3 + // either -- only above it. let (events, floor) = tail_with_floor_impl(&tail, 2); assert_eq!(cursors(&events), vec![4, 5]); - assert_eq!(floor, 3); + assert_eq!(floor, 1); } #[test] fn tail_with_floor_impl_zero_max_excludes_everything() { let tail = tail_of(1, 5); - // Nothing returned; the floor must cover every event that exists, - // not just the oldest one -- otherwise a sibling source could still - // treat seq 4 or 5 as safely skippable. + // Nothing returned; every event is excluded, so the floor is the + // oldest of them, 1 -- the smallest seq this bus cannot vouch for. let (events, floor) = tail_with_floor_impl(&tail, 0); assert!(events.is_empty()); - assert_eq!(floor, 5); + assert_eq!(floor, 1); } /// `snapshot_after` must take the same lock `publish` holds across its own @@ -874,7 +885,7 @@ mod tests { } let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(cursors(&events), vec![4, 5]); - assert_eq!(floor, 3); + assert_eq!(floor, 1); // Wide enough to cover everything: no floor. let (events, floor) = bus.tail_with_floor(sandbox_id, 10); @@ -897,8 +908,8 @@ mod tests { } let (events, floor) = platform.tail_with_floor(sandbox_id, 0); assert!(events.is_empty()); - // Nothing returned: floor must cover every event published so far. - assert_eq!(floor, 5); + // Nothing returned: floor is the oldest of everything excluded, 1. + assert_eq!(floor, 1); } #[test] @@ -1187,7 +1198,7 @@ mod tests { // Tail with smaller max should return most recent events let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(events.len(), 2); - assert_eq!(floor, 3, "newest excluded event is Event2 (seq 3)"); + assert_eq!(floor, 1, "oldest excluded event is Event0 (seq 1)"); if let Some(sandbox_stream_event::Payload::Event(ref e)) = events[0].event.payload { assert_eq!(e.reason, "Event3"); } From a3ef97b4caf74a28e802e3d0660261ed343e745c Mon Sep 17 00:00:00 2001 From: letv1nnn Date: Wed, 30 Sep 2026 13:44:13 +0100 Subject: [PATCH 06/31] fix(server): keep the delivered window above the coverage floor The floor was the oldest retained seq and the clamp kept only events below it, so any truncated followed source withheld its whole initial batch, including single-source watches such as openshell logs --tail. Use the newest excluded seq and keep events above the highest floor across followed sources. Signed-off-by: letv1nnn --- architecture/gateway.md | 25 ++- crates/openshell-server/src/grpc/sandbox.rs | 219 +++++++++++++------- crates/openshell-server/src/tracing_bus.rs | 62 +++--- 3 files changed, 186 insertions(+), 120 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index e34f4e37d8..d61bd20ed8 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -550,19 +550,18 @@ sent. `log_tail_lines` and `event_tail` bound the two tails independently, so their depths are routinely asymmetric — `event_tail` has no default, so `follow_events` without setting it replays no platform backlog at all. On connect, the server -computes each followed source's **coverage floor** — the *oldest* event that -source's own window excluded, i.e. the smallest seq it cannot vouch for — and -withholds every event, from either source, at or above the smallest nonzero -floor. It has to be the oldest, not the newest: a source's own excluded set is a -prefix of its own tail, but a sibling source's event can carry a seq strictly -between that source's oldest and newest excluded items. A boundary drawn at the -newest excluded item would let such a sibling event pass as safe, and handing it -out would still let the client's cursor outrun the source's older, still- -unreplayed backlog underneath it. - -This can withhold far more than either depth parameter alone implies — even the -entire batch — whenever a followed sibling has any backlog the request didn't -ask to replay. That is a deliberate trade-off: an emptier initial batch is +computes each followed source's **coverage floor** — the *newest* event that +source's own window excluded — and withholds every event, from either source, +at or below the highest floor. A source's excluded set is a prefix of its own +tail, so it delivered everything it retains above its floor; above the highest +floor, the batch therefore contains every event from every followed source. +Events at or below it are outside the window, the same as ordinary tail +truncation. A source never withholds its own window this way, so a +single-source watch is unaffected. + +With asymmetric depths this can deliver fewer events than the deeper source +asked for — whenever the shallower sibling left out an event newer than part of +the deeper window. That is a deliberate trade-off: a shorter initial batch is preferable to a resume that silently and permanently drops events. Withholding alone would just move the silent loss from resume time to a live diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index b28c0fa83d..6e717c51b2 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -2020,27 +2020,23 @@ pub(super) async fn handle_watch_sandbox( // to that point. // // `tail_with_floor` reports each source's own coverage floor: - // the *oldest* event its window excluded, i.e. the smallest - // seq that source cannot vouch for. It has to be the oldest, - // not the newest: a source's own excluded set is a prefix of - // its own tail, so a sibling source's event can carry a seq - // that falls strictly between this source's oldest and - // newest excluded items. A boundary at the newest excluded - // item would let that sibling event pass as safe, and - // delivering it would still let the client's remembered - // cursor reach past the excluded range -- a later resume - // would then skip the older withheld events in that range - // with no gap reported, since nothing was evicted, just - // never sent. The nearest such point across both followed - // sources bounds what this whole batch may hand out -- not - // only the deeper source's excess over the shallower one. + // the *newest* event its window excluded. The source's + // excluded set is a prefix of its own tail, so it delivered + // everything it retains above the floor. The batch as a whole + // is therefore safe above the highest floor across followed + // sources: every event from either source past that point is + // in the batch, and everything at or below it is outside the + // window, the same as ordinary tail truncation. A source never + // withholds its own window this way; only a sibling's deeper + // window can be cut back to the shallower source's floor. // - // This can mean far fewer events than `log_tail_lines` / - // `event_tail` asked for -- even zero -- whenever a followed - // sibling has any unreplayed backlog (e.g. `event_tail` - // unset, its default, on a sandbox with platform history). - // That's the deliberate trade-off of a single shared scalar - // cursor: silent loss is worse than an emptier initial batch. + // That can mean fewer events than the deeper source's depth + // asked for when a followed sibling has newer unreplayed + // backlog (e.g. `event_tail` unset, its default, with a + // platform event newer than some of the requested logs). + // That's the trade-off of a single shared scalar cursor: an + // event handed out below the sibling's floor would let a + // resume skip the sibling's withheld events past it. let mut logs = Vec::new(); let log_floor = if follow_logs { let (l, floor) = state @@ -2064,27 +2060,16 @@ pub(super) async fn handle_watch_sandbox( 0_u64 }; - // 0 means "nothing excluded" for a source, not "excluded up - // to 0" -- exclude it from the minimum so a fully-covered - // source never becomes the binding constraint. - let critical_floor = [ - follow_logs.then_some(log_floor), - follow_events.then_some(platform_floor), - ] - .into_iter() - .flatten() - .filter(|&floor| floor > 0) - .min(); - - let mut log_withheld = 0; - let platform_withheld = critical_floor.map_or(0, |cap| { - let before = logs.len(); - logs.retain(|cursored| cursored.seq < cap); - log_withheld = before - logs.len(); - let before = events.len(); - events.retain(|cursored| cursored.seq < cap); - before - events.len() - }); + // 0 means "nothing excluded" for a source, so a fully-covered + // source never raises the maximum. + let critical_floor = log_floor.max(platform_floor); + + let before = logs.len(); + logs.retain(|cursored| cursored.seq > critical_floor); + let log_withheld = before - logs.len(); + let before = events.len(); + events.retain(|cursored| cursored.seq > critical_floor); + let platform_withheld = before - events.len(); // Cutoffs reflect only what this batch actually delivered. // Withheld backlog was already published before this @@ -2112,8 +2097,8 @@ pub(super) async fn handle_watch_sandbox( // stream indefinitely, since the backlog behind the floor // was published before subscribe and will never arrive live // to fill it in. The client has to learn the gap exists from - // this warning; a resume from any cursor at or above the - // floor cannot recover it, and `tail_after` reports no gap + // this warning; a resume from any cursor this batch hands out + // cannot recover it, and `tail_after` reports no gap // when asked, since nothing was evicted, only never sent. if log_withheld > 0 || platform_withheld > 0 { let warning = crate::sandbox_watch::coverage_gap_warning_event( @@ -4639,17 +4624,55 @@ mod tests { assert_eq!(got, vec![1, 2, 3, 4]); } - /// The A2 repro: a platform event exists (seq 1) that `event_tail: 0` - /// deliberately withholds from the initial batch, while `follow_logs` - /// delivers two later logs (seq 2, 3). Without the cross-source - /// coverage floor, the client would see cursors 2 and 3, remember 3 as - /// its high-water mark, and a future resume from 3 would exclude the - /// platform event forever -- `tail_after` reports no gap, since nothing - /// was evicted, it was just never sent. The floor must instead withhold - /// the logs too, since platform's own floor (1) is the nearest point - /// neither source can vouch past. + /// Regression for the inverted coverage-floor clamp: a single followed + /// source with more buffered events than `log_tail_lines` must still + /// deliver its newest window. A source never withholds its own window, + /// and the older lines it excluded are ordinary tail truncation, so no + /// coverage-gap warning is due either. #[tokio::test] - async fn initial_tail_withholds_logs_behind_a_sibling_sources_unreplayed_backlog() { + async fn initial_tail_delivers_a_truncated_single_source_window() { + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("truncated", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 5); // cursors 1..=5 + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + log_tail_lines: 2, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut got = Vec::new(); + for _ in 0..2 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![4, 5]); + + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!(next.is_err(), "expected nothing further, got {next:?}"); + } + + /// A sibling's excluded backlog that is older than everything the deeper + /// source delivers imposes nothing: `event_tail: 0` leaves out platform + /// seq 1, but logs 2 and 3 both sit above it, so a resume from 3 skips + /// only an event outside the requested window. + #[tokio::test] + async fn initial_tail_delivers_logs_newer_than_a_sibling_sources_unreplayed_backlog() { use tokio_stream::StreamExt as _; let state = test_server_state().await; @@ -4679,25 +4702,78 @@ mod tests { let snap = stream.next().await.unwrap().unwrap(); assert!(snap.cursor.is_empty()); - // Platform's floor (1) is the binding constraint: nothing at or - // above it may be handed out, so both logs are withheld even though - // `log_tail_lines` alone would have covered them. The client learns - // about it from an explicit warning rather than discovering it - // silently on a later reconnect. + let mut got = Vec::new(); + for _ in 0..2 { + got.push(seq_of(&stream.next().await.unwrap().unwrap())); + } + assert_eq!(got, vec![2, 3]); + + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; + assert!( + next.is_err(), + "expected no warning or further events, got {next:?}" + ); + } + + /// The A2 repro: a platform event (seq 2) that `event_tail: 0` leaves + /// out sits between two logs. Handing out log 3 would let the client + /// remember cursor 3, and a resume from 3 would exclude the platform + /// event forever -- `tail_after` reports no gap, since nothing was + /// evicted, it was just never sent. Platform's floor (2) instead cuts the + /// log window back to seq 3 and withholds log 1 with a warning. + #[tokio::test] + async fn initial_tail_withholds_logs_behind_a_sibling_sources_unreplayed_backlog() { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let state = test_server_state().await; + let sandbox = test_sandbox("floorcut", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1, withheld (at or below floor) + seed_platform_event(&state, &id, "e2"); // cursor 2, never replayed (event_tail: 0) + seed_log_lines(&state, &id, 1); // cursor 3, delivered + + let response = handle_watch_sandbox( + &state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail: 0, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + // The client learns about the withheld log from an explicit warning + // rather than discovering it silently on a later reconnect. let warning = stream.next().await.unwrap().unwrap(); match warning.payload { - Some(openshell_core::proto::sandbox_stream_event::Payload::Warning(w)) => { - assert!(w.message.contains('2'), "message: {}", w.message); + Some(Payload::Warning(w)) => { + assert!( + w.message + .contains("withheld 1 log line(s) and 0 platform event(s)"), + "message: {}", + w.message + ); } other => panic!("expected a coverage-gap warning, got {other:?}"), } assert!(warning.cursor.is_empty()); + let delivered = stream.next().await.unwrap().unwrap(); + assert_eq!(seq_of(&delivered), 3); + let next = tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await; - assert!( - next.is_err(), - "expected nothing further after the warning, got {next:?}" - ); + assert!(next.is_err(), "expected nothing further, got {next:?}"); } /// The coverage-gap warning discloses the withheld backlog once, up @@ -4717,8 +4793,8 @@ mod tests { state.store.put_message(&sandbox).await.unwrap(); let id = sandbox.object_id().to_string(); - seed_platform_event(&state, &id, "e1"); // cursor 1, withheld (event_tail: 0) - seed_log_lines(&state, &id, 1); // cursor 2, withheld too (below critical_floor) + seed_log_lines(&state, &id, 1); // cursor 1, withheld (below critical_floor) + seed_platform_event(&state, &id, "e2"); // cursor 2, withheld (event_tail: 0) let response = handle_watch_sandbox( &state, @@ -4753,16 +4829,11 @@ mod tests { assert_eq!(seq_of(&live), 3); } - /// Regression for the oldest-vs-newest-excluded floor bug. - /// /// `event_tail: 0` excludes *both* of the platform bus's own events /// (seqs 1 and 3); a log event (seq 2) sits interleaved between them and - /// is otherwise fully covered by `log_tail_lines`. A floor drawn at the - /// *newest* excluded platform seq (3) would let the log event (2 < 3) - /// pass as safe -- delivering it lets the client remember cursor 2, and - /// a resume from 2 would then permanently skip platform's seq 1, which - /// was never evicted and never sent. The floor must be the *oldest* - /// excluded seq (1) so the interleaved log event is withheld too. + /// is otherwise fully covered by `log_tail_lines`. Delivering it would let + /// the client remember cursor 2 while platform's seq 3 was never sent, so + /// the floor (3, the newest excluded platform seq) withholds it too. #[tokio::test] async fn initial_tail_withholds_an_interleaved_sibling_event_between_two_excluded_seqs() { use tokio_stream::StreamExt as _; diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index ae51bd0947..32515d301c 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -190,33 +190,30 @@ fn tail_after_impl( /// that bound leaves behind. /// /// `max` may return fewer events than the tail currently retains; the ones -/// excluded are the oldest, at the front. The floor is the *oldest* excluded -/// event's seq -- the smallest seq this call cannot vouch for. `0` means -/// `max` covered everything currently retained, so there's nothing this call -/// withheld. +/// excluded are the oldest, at the front. The floor is the *newest* excluded +/// event's seq: this bus returned every event it retains above the floor and +/// none at or below it. `0` means `max` covered everything currently +/// retained, so there's nothing this call withheld. /// -/// It must be the oldest, not the newest, excluded seq. This bus's own -/// excluded set is a prefix of its own retained deque, so nothing above the -/// newest excluded item is ever missing *from this bus* -- but a sibling -/// bus's event can carry a seq that falls strictly between this bus's oldest -/// and newest excluded items, since the two buses interleave in one shared -/// seq space. A boundary drawn at the newest excluded item would let that -/// sibling event pass as safe, when delivering it would still let a client's -/// cursor reach past this bus's oldest withheld item on a later resume. +/// Because the excluded set is a prefix of this bus's retained deque, the +/// floor is the only boundary a caller needs: an event from any followed +/// source with a seq above the floor is safe to hand out as far as this bus +/// is concerned, and one at or below it may sit next to an event this bus +/// withheld. Excluding events below the delivered window is ordinary tail +/// truncation, the same as a single-source `log_tail_lines`. /// /// This is what lets a caller answer "is a cursor from this batch safe to /// resume from," which `tail_after`'s eviction-only gap check can't: a /// shallow request and a bus eviction both withhold events, but only the /// eviction leaves a trace in `last_trimmed_seq`. fn tail_with_floor_impl(tail: &VecDeque, max: usize) -> (Vec, u64) { - let total = tail.len(); - let events: Vec = tail.iter().rev().take(max).cloned().collect(); - let floor = if events.len() >= total { - 0 - } else { - tail.front().map_or(0, |cursored| cursored.seq) - }; - (events.into_iter().rev().collect(), floor) + let excluded = tail.len().saturating_sub(max); + let floor = excluded + .checked_sub(1) + .and_then(|newest_excluded| tail.get(newest_excluded)) + .map_or(0, |cursored| cursored.seq); + let events = tail.iter().skip(excluded).cloned().collect(); + (events, floor) } impl Default for TracingLogBus { @@ -630,26 +627,25 @@ mod tests { } #[test] - fn tail_with_floor_impl_truncated_reports_oldest_excluded_seq() { + fn tail_with_floor_impl_truncated_reports_newest_excluded_seq() { let tail = tail_of(1, 5); - // Newest 2 returned (4, 5); 1..=3 excluded, oldest of those is 1. - // Must be the oldest: a sibling source's event carrying seq 2 or 3 - // would otherwise pass a boundary drawn at 3 (the newest excluded) - // as safe, even though this bus hasn't vouched for anything below 3 - // either -- only above it. + // Newest 2 returned (4, 5); 1..=3 excluded, newest of those is 3. + // Every returned event sits above the floor, so a bus never + // withholds its own delivered window. let (events, floor) = tail_with_floor_impl(&tail, 2); assert_eq!(cursors(&events), vec![4, 5]); - assert_eq!(floor, 1); + assert_eq!(floor, 3); + assert!(events.iter().all(|cursored| cursored.seq > floor)); } #[test] fn tail_with_floor_impl_zero_max_excludes_everything() { let tail = tail_of(1, 5); // Nothing returned; every event is excluded, so the floor is the - // oldest of them, 1 -- the smallest seq this bus cannot vouch for. + // newest of them, 5. let (events, floor) = tail_with_floor_impl(&tail, 0); assert!(events.is_empty()); - assert_eq!(floor, 1); + assert_eq!(floor, 5); } /// `snapshot_after` must take the same lock `publish` holds across its own @@ -885,7 +881,7 @@ mod tests { } let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(cursors(&events), vec![4, 5]); - assert_eq!(floor, 1); + assert_eq!(floor, 3); // Wide enough to cover everything: no floor. let (events, floor) = bus.tail_with_floor(sandbox_id, 10); @@ -908,8 +904,8 @@ mod tests { } let (events, floor) = platform.tail_with_floor(sandbox_id, 0); assert!(events.is_empty()); - // Nothing returned: floor is the oldest of everything excluded, 1. - assert_eq!(floor, 1); + // Nothing returned: floor is the newest of everything excluded, 5. + assert_eq!(floor, 5); } #[test] @@ -1198,7 +1194,7 @@ mod tests { // Tail with smaller max should return most recent events let (events, floor) = bus.tail_with_floor(sandbox_id, 2); assert_eq!(events.len(), 2); - assert_eq!(floor, 1, "oldest excluded event is Event0 (seq 1)"); + assert_eq!(floor, 3, "newest excluded event is Event2 (seq 3)"); if let Some(sandbox_stream_event::Payload::Event(ref e)) = events[0].event.payload { assert_eq!(e.reason, "Event3"); } From 84c29f94e18cafc9948a856680a3dfae4dbef73b Mon Sep 17 00:00:00 2001 From: letv1nnn Date: Wed, 30 Sep 2026 14:12:34 +0100 Subject: [PATCH 07/31] fix(server): snapshot initial watch tails under one lock Read both initial tails and the cursor-space high-water mark under the publication lock, and suppress live copies at or below that mark, so a publish between the two reads can no longer emit a higher cursor ahead of a lower event. Signed-off-by: letv1nnn --- architecture/gateway.md | 42 ++--- crates/openshell-server/src/grpc/sandbox.rs | 112 ++++++------ crates/openshell-server/src/tracing_bus.rs | 185 ++++++++++++++++---- 3 files changed, 228 insertions(+), 111 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index d61bd20ed8..e303b8e0a2 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -515,26 +515,28 @@ whatever each bus kept; it does not align their depths. The broadcast receivers are subscribed before replay, so an event buffered during initialization could appear in both replay and the live receiver; the producer -tracks the highest replayed seq and suppresses live events at or below it, so -each event is delivered once. That mark is per source, because the two tails are -read at different instants and bounded independently — one shared mark would let -the deeper source censor the shallower one, discarding the shallower source's own -live arrivals as duplicates of a replay that never ran. Subscribing never mints a -cursor space, so a resume against a torn-down sandbox cannot create the space its -stale cursor is then checked against. Clients track the highest observed `cursor` -and pass it as `resume_after_cursor` on reconnect. - -On resume, epoch validation and both bus reads happen inside one call under one -lock — `TracingLogBus::snapshot_after` — rather than as a validate-then-read -pair. A publish landing between two independently-locked reads would be visible -to whichever ran second and not the other, desyncing their high-water marks -against a live stream that treats them as read at the same instant; a teardown -plus a republish landing between a *separate* validation and read would apply -the old space's seq to the replacement's buffers and find no gap, since -`tail_after` only compares numbers, not epochs. Folding validation into the same -lock hold as the reads closes both windows at once: nothing can retire or -recreate the space while it's held, so a second post-read epoch check isn't -needed. +keeps a per-source mark and suppresses live events at or below it, so each event +is delivered once. Subscribing never mints a cursor space, so a resume against a +torn-down sandbox cannot create the space its stale cursor is then checked +against. Clients track the highest observed `cursor` and pass it as +`resume_after_cursor` on reconnect. + +Both replay paths read the two buses inside one call under the cursor-space lock +that every publish holds across its buffer insert and broadcast send: +`TracingLogBus::snapshot_after` on resume and `TracingLogBus::snapshot_tail` +without a cursor. A publish landing between two independently locked reads would +let the second read hand out a higher cursor while a lower event from the other +source arrives live afterwards, and a client that disconnected in between would +resume past it. On resume, the same lock hold also validates the epoch, so a +teardown plus a republish cannot retire the validated space and install a +replacement numbered from 1 before the reads apply the old seq to it. + +The marks differ by path. A resume replays everything after the cursor, so each +source's mark is the last seq its own replay returned. The initial tail is +bounded by depth, so both marks move to the snapshot's high-water mark: every +event at or below it was already buffered, and was either sent in the batch or +left out as pre-snapshot history. Its live copy is suppressed rather than +delivered after a higher cursor from the batch. #### Coverage floor diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 6e717c51b2..6c16bbb708 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1871,24 +1871,22 @@ pub(super) async fn handle_watch_sandbox( // replay buffer and a live receiver; the live loop suppresses events // at or below its source's mark so each is delivered exactly once. // - // The two marks must stay separate. Both buses number from one - // shared cursor space, but they are read at different instants and - // bounded independently (`log_tail_lines` vs `event_tail`, which has - // no default and so replays nothing unless the client asks). A - // single shared mark therefore lets the deeper source censor the - // shallower one: with the default `event_tail` of 0 the mark rises - // to the newest buffered log while no platform event was replayed at - // all, and every platform event published in the initialization - // window is dropped as a duplicate of something never sent. Keyed by - // source, an event is suppressed only if its own source's replay - // actually covered it. + // Both reads happen under one cursor-space lock hold, so every + // event at or below the snapshot's high-water mark was buffered + // when they ran. A resume replays everything after the cursor from + // each followed source, so each source's mark is the last seq its + // own replay returned. The initial tail is depth-bounded, so both + // marks move to the snapshot's high-water mark instead: anything at + // or below it was either in the batch or deliberately left out as + // pre-snapshot history, and its live copy must not arrive after a + // higher cursor from the batch. // - // No unit test pins this. The only reachable window is between the - // subscribe above and the log tail read below -- an event published - // earlier is replayed rather than live, and one published later - // outranks the mark -- and the producer crosses that window with no - // await a test can wedge open. Reproducing it needs a seam in the - // producer, which is not worth adding to production code. + // No gateway-level test reaches the window between the subscribe + // above and the snapshot below: the producer crosses it with no + // await a test can wedge open. The bus-level + // `snapshot_tail_blocks_while_cursor_space_is_locked` and + // `snapshot_after_blocks_while_cursor_space_is_locked` pin the + // atomicity this relies on. let resume_seq = resume_after.map_or(0, |resume| resume.seq); let mut log_cutoff: u64 = resume_seq; let mut platform_cutoff: u64 = resume_seq; @@ -2037,28 +2035,24 @@ pub(super) async fn handle_watch_sandbox( // That's the trade-off of a single shared scalar cursor: an // event handed out below the sibling's floor would let a // resume skip the sibling's withheld events past it. - let mut logs = Vec::new(); - let log_floor = if follow_logs { - let (l, floor) = state - .tracing_log_bus - .tail_with_floor(&sandbox_id, log_tail as usize); - logs = l; - floor - } else { - 0_u64 - }; - - let mut events = Vec::new(); - let platform_floor = if follow_events { - let (e, floor) = state - .tracing_log_bus - .platform_event_bus - .tail_with_floor(&sandbox_id, event_tail as usize); - events = e; - floor - } else { - 0_u64 - }; + // + // Both windows and the space's high-water mark come from one + // lock hold (see `TracingLogBus::snapshot_tail`), so no + // publish can land between the two reads. Read separately, a + // log published after the log read and a platform event + // published after it could let the platform window hand out + // the higher cursor while the log arrives live afterwards. + let crate::tracing_bus::TailSnapshot { + mut logs, + log_floor, + mut events, + platform_floor, + highest_seq, + } = state.tracing_log_bus.snapshot_tail( + &sandbox_id, + follow_logs.then_some(log_tail as usize), + follow_events.then_some(event_tail as usize), + ); // 0 means "nothing excluded" for a source, so a fully-covered // source never raises the maximum. @@ -2071,35 +2065,31 @@ pub(super) async fn handle_watch_sandbox( events.retain(|cursored| cursored.seq > critical_floor); let platform_withheld = before - events.len(); - // Cutoffs reflect only what this batch actually delivered. - // Withheld backlog was already published before this - // connect, so it can never arrive again via the live - // broadcast -- there's nothing to suppress a duplicate of, - // and raising a cutoff past what was sent would risk - // swallowing a genuinely new live event with a seq in the - // withheld range instead. + // Both cutoffs move to the snapshot's high-water mark. The + // receivers were subscribed before the snapshot, so an event + // published in between is both buffered and queued live. At + // or below the mark, it was either delivered above or left + // out of this batch as pre-snapshot history; delivering its + // live copy would emit a lower cursor after a higher one + // from the batch. Above the mark, it can only arrive live. + log_cutoff = log_cutoff.max(highest_seq); + platform_cutoff = platform_cutoff.max(highest_seq); + let mut tail: Vec = Vec::new(); - if let Some(last) = logs.last() { - log_cutoff = log_cutoff.max(last.seq); - } tail.extend(logs); - if let Some(last) = events.last() { - platform_cutoff = platform_cutoff.max(last.seq); - } tail.extend(events); tail.sort_by_key(|cursored| cursored.seq); // Disclose the gap before anything else in this batch. The - // cutoffs above are never raised past what was actually - // delivered, so a later live event past the floor still - // reaches the client -- withholding it too would starve the - // stream indefinitely, since the backlog behind the floor - // was published before subscribe and will never arrive live - // to fill it in. The client has to learn the gap exists from - // this warning; a resume from any cursor this batch hands out - // cannot recover it, and `tail_after` reports no gap - // when asked, since nothing was evicted, only never sent. + // withheld events sit at or below the snapshot's mark, so the + // cutoffs above keep their live copies (if any) from being + // delivered out of order, while later live events past the + // mark still reach the client. The client has to learn the + // gap exists from this warning; a resume from any cursor this + // batch hands out cannot recover it, and `tail_after` reports + // no gap when asked, since nothing was evicted, only never + // sent. if log_withheld > 0 || platform_withheld > 0 { let warning = crate::sandbox_watch::coverage_gap_warning_event( log_withheld, diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index 32515d301c..bfb506f100 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -299,24 +299,6 @@ impl TracingLogBus { .collect::>() } - /// Like `tail`, but also reports the coverage floor `max` leaves behind. - /// - /// See `tail_with_floor_impl`: a cursor at or below the returned floor - /// isn't safe to advertise as a resume point, because `max` -- not - /// eviction -- is why anything at or below it is missing from this - /// batch. - pub(crate) fn tail_with_floor( - &self, - sandbox_id: &str, - max: usize, - ) -> (Vec, u64) { - let inner = self.inner.lock().expect("tracing bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || (Vec::new(), 0), - |per| tail_with_floor_impl(&per.tail, max), - ) - } - /// Identity and extent of this sandbox's current cursor space. /// /// `None` means nothing has been published for the sandbox, so there is no @@ -428,6 +410,77 @@ impl TracingLogBus { }); ResumeSnapshot::Read(log, platform) } + + /// Read both bounded initial tails and the space's high-water mark, all + /// under one lock hold. + /// + /// `None` skips a source. Holding the cursor-space lock -- the one every + /// publish holds across its tail insert and broadcast send -- means no + /// publish can land between the two reads, and `highest_seq` is exactly + /// the boundary between what these tails could see and what can only + /// arrive live. Every event at or below it was already buffered when the + /// tails were read, so the caller can suppress live copies at or below it + /// without dropping anything the tails could not have covered. + /// + /// Reads never mint a space: with none, both tails are empty and + /// `highest_seq` is `0`. + pub(crate) fn snapshot_tail( + &self, + sandbox_id: &str, + log_max: Option, + platform_max: Option, + ) -> TailSnapshot { + let spaces = self.seq.lock(); + let highest_seq = spaces + .get(sandbox_id) + .map_or(0, |space| space.next.saturating_sub(1)); + + let (logs, log_floor) = log_max.map_or_else( + || (Vec::new(), 0), + |max| { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || (Vec::new(), 0), + |per| tail_with_floor_impl(&per.tail, max), + ) + }, + ); + let (events, platform_floor) = platform_max.map_or_else( + || (Vec::new(), 0), + |max| { + let inner = self + .platform_event_bus + .inner + .lock() + .expect("platform event bus lock poisoned"); + inner.per_id.get(sandbox_id).map_or_else( + || (Vec::new(), 0), + |per| tail_with_floor_impl(&per.tail, max), + ) + }, + ); + drop(spaces); + + TailSnapshot { + logs, + log_floor, + events, + platform_floor, + highest_seq, + } + } +} + +/// Outcome of [`TracingLogBus::snapshot_tail`]. Each floor is the one +/// `tail_with_floor_impl` reports for that source; an unread source has no +/// events and a floor of `0`. +pub(crate) struct TailSnapshot { + pub(crate) logs: Vec, + pub(crate) log_floor: u64, + pub(crate) events: Vec, + pub(crate) platform_floor: u64, + /// Highest seq the space had issued at the lock hold; `0` with no space. + pub(crate) highest_seq: u64, } /// Outcome of [`TracingLogBus::snapshot_after`]. @@ -873,25 +926,95 @@ mod tests { } #[test] - fn tracing_log_bus_tail_with_floor_reports_truncation() { + fn snapshot_tail_reports_truncation() { let bus = TracingLogBus::new(); let sandbox_id = "sb-floor"; for _ in 0..5 { bus.publish_external(make_log_event(sandbox_id, "line")); } - let (events, floor) = bus.tail_with_floor(sandbox_id, 2); - assert_eq!(cursors(&events), vec![4, 5]); - assert_eq!(floor, 3); + let snap = bus.snapshot_tail(sandbox_id, Some(2), None); + assert_eq!(cursors(&snap.logs), vec![4, 5]); + assert_eq!(snap.log_floor, 3); // Wide enough to cover everything: no floor. - let (events, floor) = bus.tail_with_floor(sandbox_id, 10); - assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); - assert_eq!(floor, 0); + let snap = bus.snapshot_tail(sandbox_id, Some(10), None); + assert_eq!(cursors(&snap.logs), vec![1, 2, 3, 4, 5]); + assert_eq!(snap.log_floor, 0); + } - // Unknown sandbox: nothing buffered, no floor. - let (events, floor) = bus.tail_with_floor("nope", 10); - assert!(events.is_empty()); - assert_eq!(floor, 0); + /// `highest_seq` is the space's high-water mark at the lock hold, not the + /// newest event either window returned: events a depth excluded still + /// count, since they were buffered when the tails were read and can only + /// reach a live receiver as a duplicate of pre-snapshot history. + #[test] + fn snapshot_tail_reports_the_high_water_mark_past_excluded_events() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-hwm"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 3 + + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0)); + assert_eq!(cursors(&snap.logs), vec![1, 2]); + assert_eq!(snap.log_floor, 0); + assert!(snap.events.is_empty()); + assert_eq!(snap.platform_floor, 3); + assert_eq!(snap.highest_seq, 3); + } + + /// An unread source is skipped outright; a sandbox with nothing published + /// yields empty tails and a zero mark without minting a cursor space. + #[test] + fn snapshot_tail_skips_unread_sources_and_never_mints_a_space() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-tail-skip"; + bus.publish_external(make_log_event(sandbox_id, "l1")); + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); + + let snap = bus.snapshot_tail(sandbox_id, Some(10), None); + assert_eq!(cursors(&snap.logs), vec![1]); + assert!(snap.events.is_empty()); + assert_eq!(snap.platform_floor, 0); + assert_eq!(snap.highest_seq, 2); + + let snap = bus.snapshot_tail("nope", Some(10), Some(10)); + assert!(snap.logs.is_empty() && snap.events.is_empty()); + assert_eq!(snap.highest_seq, 0); + assert_eq!(bus.cursor_space("nope"), None); + } + + /// Same guarantee as `snapshot_after_blocks_while_cursor_space_is_locked` + /// for the initial read: `snapshot_tail` takes the lock every publish holds + /// across its tail insert and broadcast send, so no publish can land + /// between the log read and the platform read. + #[test] + fn snapshot_tail_blocks_while_cursor_space_is_locked() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-tail-lock"; + bus.publish_external(make_log_event(sandbox_id, "line")); + + let guard = bus.seq.lock(); + + let bus2 = bus.clone(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let handle = std::thread::spawn(move || { + bus2.snapshot_tail(sandbox_id, Some(10), Some(10)); + done_tx.send(()).unwrap(); + }); + + // Give the worker time to reach (and block on) the lock. + std::thread::sleep(std::time::Duration::from_millis(50)); + assert_eq!( + done_rx.try_recv(), + Err(std::sync::mpsc::TryRecvError::Empty), + "snapshot_tail returned without waiting for the cursor-space lock" + ); + + drop(guard); + done_rx + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("snapshot_tail should complete once the lock is released"); + handle.join().unwrap(); } #[test] @@ -1292,7 +1415,9 @@ impl PlatformEventBus { } /// Return buffered platform events and the coverage floor `max` leaves - /// behind. See `TracingLogBus::tail_with_floor`. + /// behind. See `tail_with_floor_impl`. Production reads go through + /// `TracingLogBus::snapshot_tail`, which holds the cursor-space lock. + #[cfg(test)] pub(crate) fn tail_with_floor( &self, sandbox_id: &str, From 0a86765a86904a285c7954fe8b0118cc1325a8ec Mon Sep 17 00:00:00 2001 From: letv1nnn Date: Wed, 30 Sep 2026 22:54:59 +0100 Subject: [PATCH 08/31] docs(server): correct equal-depth coverage floor claim and add tests Signed-off-by: letv1nnn --- architecture/gateway.md | 42 +++--- crates/openshell-server/src/grpc/sandbox.rs | 134 ++++++++++++++++++-- docs/observability/accessing-logs.mdx | 2 +- proto/openshell.proto | 23 +--- sdk/go/proto/openshellv1/openshell.pb.go | 7 + 5 files changed, 161 insertions(+), 47 deletions(-) diff --git a/architecture/gateway.md b/architecture/gateway.md index e303b8e0a2..2b00865704 100644 --- a/architecture/gateway.md +++ b/architecture/gateway.md @@ -549,9 +549,11 @@ or `event_tail`), and anything a shallower source withheld below that cursor is excluded forever with no gap reported, since it was never evicted, just never sent. -`log_tail_lines` and `event_tail` bound the two tails independently, so their -depths are routinely asymmetric — `event_tail` has no default, so `follow_events` -without setting it replays no platform backlog at all. On connect, the server +`log_tail_lines` and `event_tail` bound the two tails independently, and the two +sources interleave in the shared cursor order, so one source's window can leave +out an event that is newer than part of the other's — `event_tail` has no +default, so `follow_events` without setting it replays no platform backlog at +all. On connect, the server computes each followed source's **coverage floor** — the *newest* event that source's own window excluded — and withholds every event, from either source, at or below the highest floor. A source's excluded set is a prefix of its own @@ -561,23 +563,23 @@ Events at or below it are outside the window, the same as ordinary tail truncation. A source never withholds its own window this way, so a single-source watch is unaffected. -With asymmetric depths this can deliver fewer events than the deeper source -asked for — whenever the shallower sibling left out an event newer than part of -the deeper window. That is a deliberate trade-off: a shorter initial batch is -preferable to a resume that silently and permanently drops events. - -Withholding alone would just move the silent loss from resume time to a live -event delivered moments later: nothing raises `log_cutoff`/`platform_cutoff` -past what this batch actually sent, so the very next live event past the floor -would still be delivered and would still let the client's cursor outrun the -withheld backlog. Refusing that delivery too doesn't help either — the withheld -events were published before subscribe and will never arrive live to fill the -gap in, so gating every later event on them would starve the stream -indefinitely instead of surfacing one bounded gap. The producer instead emits a -coverage-gap warning ahead of the batch, naming how many events were withheld -per source, and then proceeds with ordinary cutoffs. The gap is disclosed once, -up front, rather than silently, which is the bar the whole loss-awareness design -holds to — not that nothing is ever lost. +This can deliver fewer events than a source's depth asked for whenever the other +followed source left out an event newer than part of its window. That depends on +where the two histories fall in the cursor order, not on whether the depths are +equal: with logs at cursors 1–2, platform events at 3–4, and both depths set to +1, platform leaves out 3, so log 2 is withheld. That is a deliberate trade-off: +a shorter initial batch is preferable to a resume that silently and permanently +drops events. + +Withheld events sit at or below the snapshot's high-water mark, so the live +cutoffs keep any live copy of them from arriving after a higher cursor, while +live events above the mark are delivered normally. Holding back later events +until the withheld ones arrive would not help: they were published before the +snapshot and never arrive live, so the stream would stall. The producer instead +emits a coverage-gap warning ahead of the batch, naming how many events were +withheld per source. The gap is disclosed once, up front, rather than silently, +which is the bar the whole loss-awareness design holds to — not that nothing is +ever lost. ## Persistence diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 6c16bbb708..ea6b0dd0aa 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -2025,13 +2025,15 @@ pub(super) async fn handle_watch_sandbox( // sources: every event from either source past that point is // in the batch, and everything at or below it is outside the // window, the same as ordinary tail truncation. A source never - // withholds its own window this way; only a sibling's deeper - // window can be cut back to the shallower source's floor. + // withholds its own window this way; only a sibling's floor + // can cut a source's window back. // - // That can mean fewer events than the deeper source's depth - // asked for when a followed sibling has newer unreplayed - // backlog (e.g. `event_tail` unset, its default, with a - // platform event newer than some of the requested logs). + // That can mean fewer events than a source's depth asked for + // whenever a followed sibling left out an event newer than + // part of its window. This depends on where the histories + // fall in the cursor order, not on the depths being unequal: + // logs at 1-2 and platform at 3-4 with both depths at 1 + // leaves out platform 3, so log 2 is withheld. // That's the trade-off of a single shared scalar cursor: an // event handed out below the sibling's floor would let a // resume skip the sibling's withheld events past it. @@ -4872,8 +4874,8 @@ mod tests { ); } - /// Symmetric depths (or a source that's fully covered) impose no floor: - /// this is the common case and must behave exactly as before A2. + /// Windows that leave nothing out impose no floor: every buffered event + /// is delivered, as before A2. #[tokio::test] async fn initial_tail_unclamped_when_no_source_has_unreplayed_backlog() { use tokio_stream::StreamExt as _; @@ -4912,6 +4914,122 @@ mod tests { assert_eq!(got, vec![1, 2, 3]); } + /// Open an initial watch following both sources with the given depths and + /// collect what the initial batch delivers: the coverage-gap warning + /// message, if any, and the delivered cursors in order. + async fn initial_tail_following_both( + state: &Arc, + sandbox: &Sandbox, + log_tail_lines: u32, + event_tail: u32, + ) -> (Option, Vec) { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let response = handle_watch_sandbox( + state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + log_tail_lines, + event_tail, + ..Default::default() + }), + ) + .await + .unwrap(); + + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty()); + + let mut warning = None; + let mut delivered = Vec::new(); + while let Ok(Some(item)) = + tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await + { + let event = item.unwrap(); + match event.payload { + Some(Payload::Warning(w)) => { + assert!(delivered.is_empty(), "warning must precede the batch"); + assert!(warning.replace(w.message).is_none(), "one warning at most"); + } + _ => delivered.push(seq_of(&event)), + } + } + (warning, delivered) + } + + /// Equal depths do not rule out trimming. Logs sit at cursors 1-2 and + /// platform events at 3-4; with both depths at 1 the platform window + /// leaves out 3, which is newer than log 2, so log 2 is withheld even + /// though `log_tail_lines` alone would have delivered it. + #[tokio::test] + async fn initial_tail_equal_depths_withhold_an_older_log_window() { + let state = test_server_state().await; + let sandbox = test_sandbox("equallogsfirst", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 2); // cursors 1, 2 + seed_platform_event(&state, &id, "e3"); // cursor 3, left out by event_tail + seed_platform_event(&state, &id, "e4"); // cursor 4 + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + let warning = warning.expect("withheld log 2 must be disclosed"); + assert!( + warning.contains("withheld 1 log line(s) and 0 platform event(s)"), + "message: {warning}" + ); + assert_eq!(delivered, vec![4]); + } + + /// The mirror case: platform events at cursors 1-2 and logs at 3-4. The + /// log window leaves out 3, which is newer than platform 2, so this time + /// the platform event is withheld. + #[tokio::test] + async fn initial_tail_equal_depths_withhold_an_older_platform_window() { + let state = test_server_state().await; + let sandbox = test_sandbox("equalplatformfirst", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_platform_event(&state, &id, "e1"); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + seed_log_lines(&state, &id, 2); // cursors 3, 4; 3 left out by log_tail_lines + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + let warning = warning.expect("withheld platform event 2 must be disclosed"); + assert!( + warning.contains("withheld 0 log line(s) and 1 platform event(s)"), + "message: {warning}" + ); + assert_eq!(delivered, vec![4]); + } + + /// Equal depths over interleaved histories withhold nothing when neither + /// window leaves out an event newer than part of the other: logs at 1 and + /// 3, platform at 2 and 4, both depths 1. Each window's excluded event is + /// older than everything delivered, so both 3 and 4 arrive, no warning. + #[tokio::test] + async fn initial_tail_equal_depths_over_interleaved_histories_withhold_nothing() { + let state = test_server_state().await; + let sandbox = test_sandbox("equalinterleaved", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1, left out by log_tail_lines + seed_platform_event(&state, &id, "e2"); // cursor 2, left out by event_tail + seed_log_lines(&state, &id, 1); // cursor 3 + seed_platform_event(&state, &id, "e4"); // cursor 4 + + let (warning, delivered) = initial_tail_following_both(&state, &sandbox, 1, 1).await; + assert_eq!(warning, None); + assert_eq!(delivered, vec![3, 4]); + } + #[tokio::test] async fn live_delivery_orders_events_across_sources_by_cursor() { use tokio_stream::StreamExt as _; diff --git a/docs/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx index 05b273264f..2f4c7a15ae 100644 --- a/docs/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -58,7 +58,7 @@ On reconnect, a client passes the highest cursor it processed as the resume poin The gateway merges the log and platform event sources before emitting, so events normally arrive in ascending cursor order. That applies to the buffered tail you receive when the stream opens, to a replay after a resume, and to live delivery. The gateway does not delay an event to wait for a lower cursor that has not been published yet, so a cursor can still arrive late under concurrent publication. Track the highest cursor seen as the resume point rather than the last one received. -A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events through the SDK and set how far back each replays independently, an asymmetric depth can reduce how much either source delivers on connect — the gateway withholds events rather than hand out a cursor that one of the two sources cannot fully vouch for, and reports what it withheld with a warning event rather than dropping it silently. `openshell logs` doesn't expose this dial and isn't affected; it applies to SDK callers who set both replay depths directly. +A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events, either source can deliver fewer events on connect than its replay depth asked for. This happens when the other source's replay leaves out an event that is newer than part of this source's history, even when both depths are equal. The gateway withholds those events rather than hand out a cursor that one of the two sources cannot fully vouch for, and reports what it withheld with a warning event rather than dropping it silently. Watches that follow only logs, such as `openshell logs`, are not affected. ## Direct Filesystem Access diff --git a/proto/openshell.proto b/proto/openshell.proto index 363941a78a..0d0a59611a 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -1999,22 +1999,13 @@ message WatchSandboxRequest { // Replay the last N log lines (best-effort) before following. // - // When both follow_logs and follow_events are set, a cursor handed back on - // this connect is only safe to resume from if *every* followed source can - // vouch it covers everything up to that point (see resume_after_cursor). - // If this depth and event_tail are asymmetric -- e.g. event_tail is left - // at its default of 0 while this sandbox has platform history -- the - // server withholds events from whichever source that would otherwise let - // a resumable cursor outrun the other source's replay, even below this - // limit. Symmetric depths, or a sandbox with no backlog on the shallower - // source, are unaffected. + // When following both sources, the replay can return fewer lines: events + // older than one the other source's replay left out are withheld and + // reported with a warning, even if both depths are equal. uint32 log_tail_lines = 5; // Replay the last N platform events (best-effort) before following. - // - // Has no default: 0 replays no platform events on connect. See - // log_tail_lines for how an asymmetric depth against that field can - // reduce what either source delivers on this connect. + // Defaults to 0. Can return fewer events; see log_tail_lines. uint32 event_tail = 6; // Stop streaming once the sandbox reaches READY or a terminal result phase @@ -2049,11 +2040,7 @@ message WatchSandboxRequest { // identically. A cursor this server could not have issued is rejected with // INVALID_ARGUMENT. // - // The cursor is a single opaque position shared across log and platform - // events; the client tracks one scalar, not one per source. When - // follow_logs and follow_events are both set, the server never hands out a - // cursor that one of the two sources cannot fully vouch for -- see - // log_tail_lines. + // One cursor covers both log and platform events. string resume_after_cursor = 12; } diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index e40dbd8082..2fa11c12e4 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -7467,8 +7467,13 @@ type WatchSandboxRequest struct { // Stream platform events correlated to this sandbox. FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` // Replay the last N log lines (best-effort) before following. + // + // When following both sources, the replay can return fewer lines: events + // older than one the other source's replay left out are withheld and + // reported with a warning, even if both depths are equal. LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` // Replay the last N platform events (best-effort) before following. + // Defaults to 0. Can return fewer events; see log_tail_lines. EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` // Stop streaming once the sandbox reaches READY or a terminal result phase // (COMPLETED, STOPPED, or ERROR). @@ -7497,6 +7502,8 @@ type WatchSandboxRequest struct { // empty resume_after_cursor, because retrying the same token fails // identically. A cursor this server could not have issued is rejected with // INVALID_ARGUMENT. + // + // One cursor covers both log and platform events. ResumeAfterCursor string `protobuf:"bytes,12,opt,name=resume_after_cursor,json=resumeAfterCursor,proto3" json:"resume_after_cursor,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache From c41f0967f674582580e0e05343aa9fb6ef272bb5 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 5 Nov 2026 12:48:33 +0000 Subject: [PATCH 09/31] docs(testing): note that tmachine runs on macOS (#5304) tmachine boots guests with the Hypervisor framework on macOS, so the runner does not need KVM there. Signed-off-by: Evan Lezar --- TESTING.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/TESTING.md b/TESTING.md index 5e74f53c72..4d0213ef90 100644 --- a/TESTING.md +++ b/TESTING.md @@ -232,14 +232,14 @@ nix run .#openshell-test-runner -- \ --family conformance --package conformance ``` -It needs KVM, and tmachine only boots guests of the host's architecture. The -runner points the tests at the installed CLI and the gateway the installer -registered (`--gateway`, default `openshell`). A run passes if nextest -succeeds; nextest fails a run with no tests. The runner exits with 1 when a -test failed and 2 when the run could not complete, for example because the -environment didn't come up. It writes `junit.xml`, `nextest.log` and, after a -failure, the guest's diagnostics to `artifacts/test-results/` (`--results`), -even when the run timed out. +It needs KVM on Linux or the Hypervisor framework on macOS, and tmachine only +boots guests of the host's architecture. The runner points the tests at the +installed CLI and the gateway the installer registered (`--gateway`, default +`openshell`). A run passes if nextest succeeds; nextest fails a run with no +tests. The runner exits with 1 when a test failed and 2 when the run could not +complete, for example because the environment didn't come up. It writes +`junit.xml`, `nextest.log` and, after a failure, the guest's diagnostics to +`artifacts/test-results/` (`--results`), even when the run timed out. `--filter ` narrows the tests with a nextest filterset, and `--keep` leaves the VM running until you press Enter. Two runs on one host From a2afbcdc4ef39f9b29477a548580b0262b90c96f Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Thu, 5 Nov 2026 13:07:10 +0000 Subject: [PATCH 10/31] test(testkit): add the openshell-conformance test program (#5249) * build(testkit): add dependencies for the test registry and program Add linkme for the test registry, libtest-mimic and clap for the test program's command line, quick-junit for its report, and libc for process control on Unix. Refresh the lockfiles of the workspaces that depend on testkit. Signed-off-by: Evan Lezar * feat(testkit): register tests and add a test program with list `#[testkit::test]` now also registers each test, with its family, name, description and `#[ignore]` reason, in a link-time registry. The hidden function gets only `cfg` and lint attributes; `#[should_panic]`, `#[cfg_attr]` and `#[expect]` are rejected with an error that says why. `testkit::main()` turns a binary into a test program. It takes libtest's arguments, so it also works as a `harness = false` target under cargo test and nextest, and it has a `list` subcommand that prints each test's name and description. A program that links no tests exits 2, since Rust does not link a suite crate that is never named. Signed-off-by: Evan Lezar * feat(testkit): add a ready subcommand to the test program `ready [--timeout SECS]` waits for the gateway the program will test and exits 2 if it is not ready in time. It shares its check with the `testkit-ready` binary, which now calls it instead of keeping its own copy. Signed-off-by: Evan Lezar * feat(testkit): add a run subcommand to the test program `run [FILTER]... [--include-ignored] [--timeout SECS] [--grace SECS]` runs the selected tests one at a time. Each test is a new process, the program started as ` --exact --nocapture`, in its own process group, so nothing a test starts outlives it. A test that runs past its timeout (900s by default) gets SIGTERM, then SIGKILL after the grace period (10s by default). Interrupting `run` stops the current test and runs no more; a second interrupt skips the grace period. `run` exits 1 when a test failed and 2 when tests could not run or the run was interrupted. It is Unix only; on other platforms it says to use cargo test or nextest. Signed-off-by: Evan Lezar * feat(testkit): write a JUnit report from the test program's run `run --junit PATH` writes a JUnit report with one test suite per family and one test case per test, including skipped, failed and timed-out tests. Each test's output is shown as it comes and the last 4 MiB of each stream is kept for the report. The supervisor waits for a test's output to close before it moves on, so a signal that arrives while it waits still ends the run. Signed-off-by: Evan Lezar * docs(testkit): document the test program and test registration Describe how `#[testkit::test]` registers tests, the `list`, `ready` and `run` subcommands, their exit codes, and the limits of `run`: Unix only, and a test stopped on a timeout does not run its cleanup. Signed-off-by: Evan Lezar * refactor(conformance): move the conformance suites into a library Cargo builds each file in tests/ as its own test executable, which no other program can link. Move the conformance suites into src/ as modules of a library, so a test program can link them. Test code is unchanged; cargo test and nextest run the same 15 tests as the library's unit tests. Nextest now names them as binary `conformance`, test `::`, so filters that selected a suite by binary select it by test name instead. Signed-off-by: Matthew Grossman Signed-off-by: Evan Lezar * feat(conformance): add the openshell-conformance program The conformance crate now also builds openshell-conformance, a binary whose main calls testkit::main() and links the crate's library. It lists, waits for the gateway, and runs the conformance tests without cargo or nextest. It runs only the conformance suite and promises only the conformance contract; feature and driver suites are not part of it. The binary has test = false, so cargo test and nextest still see one conformance test binary, the library's unit tests, and the e2e lanes and the runner's filters are unchanged. The library is linked without cfg(test), so the program gets the registrations and none of the tokio test wrappers. tasks/scripts/check-conformance-program.sh checks that openshell-conformance list names the same tests as nextest; Branch Checks and mise test:rust run it. A dependency that is never named is not linked, so a suite the program forgets to name would silently go missing. testkit::main() now fails with exit 2 when a program links no tests, under every command. Signed-off-by: Matthew Grossman * test(e2e): run the conformance host lanes with openshell-conformance The Docker, Podman, VM and Kubernetes host lanes ran the conformance suite with cargo nextest. They now run it with openshell-conformance: ready, then run --junit, so CI exercises the program that gateway owners use. The script uses OPENSHELL_CONFORMANCE_BIN when set and otherwise builds the program from the checkout. The JUnit report goes to OPENSHELL_CONFORMANCE_JUNIT or results/conformance-.xml; the Kubernetes runner names it after its report so credential-driver runs keep both. Signed-off-by: Matthew Grossman * ci(e2e): build openshell-conformance once per architecture in Branch E2E Each Docker, Podman, VM and Kubernetes host lane built the conformance suite itself. Branch E2E now builds openshell-conformance once per architecture as a static musl binary, checks that it is static and lists its tests, and uploads it. setup-e2e-cli downloads the one that matches the runner when a lane sets prebuilt-conformance, and the lane runs it through OPENSHELL_CONFORMANCE_BIN. The job is separate from build-binaries.yml, which the release workflows also call: the program has its own workspace and is not released. Lanes called without prebuilt-conformance, such as the release qualification lanes, still build the program from the checkout. Signed-off-by: Matthew Grossman * test(e2e): harden the conformance program helpers Take the openshell-conformance path from cargo's build messages instead of assuming /debug, so CARGO_BUILD_TARGET and other profiles work. Make the drift check fail clearly when it reads no test names from nextest. Signed-off-by: Matthew Grossman --------- Signed-off-by: Evan Lezar Signed-off-by: Matthew Grossman Co-authored-by: Matthew Grossman --- .github/actions/setup-e2e-cli/action.yml | 22 + .github/workflows/branch-checks.yml | 1 + .github/workflows/branch-e2e.yml | 100 +++- .github/workflows/e2e-docker-test.yml | 6 + .github/workflows/e2e-kubernetes-test.yml | 7 + .github/workflows/e2e-podman-test.yml | 6 + .github/workflows/e2e-vm-test.yml | 6 + CI.md | 2 +- TESTING.md | 11 +- e2e/policy-advisor/README.md | 2 +- e2e/rust/e2e-kubernetes.sh | 3 +- e2e/support/conformance.sh | 52 +- tasks/scripts/check-conformance-program.sh | 34 ++ tasks/test.toml | 1 + tests/runner/src/main.rs | 2 +- tests/suites/conformance/Cargo.lock | 208 +++++++ tests/suites/conformance/Cargo.toml | 10 +- tests/suites/conformance/README.md | 29 +- .../suites/conformance/{tests => src}/cli.rs | 0 .../{tests => src}/file_transfer.rs | 0 tests/suites/conformance/src/lib.rs | 16 + .../conformance/{tests => src}/lifecycle.rs | 0 tests/suites/conformance/src/main.rs | 18 + .../{tests => src}/persistent_workspace.rs | 0 .../{tests => src}/policy_advisor.rs | 0 .../conformance/{tests => src}/smoke.rs | 0 tests/suites/drivers/podman/Cargo.lock | 208 +++++++ tests/suites/features/Cargo.lock | 208 +++++++ tests/support/Cargo.lock | 208 +++++++ tests/testkit/Cargo.lock | 209 +++++++ tests/testkit/README.md | 63 ++- tests/testkit/testkit-macros/src/lib.rs | 212 ++++++- tests/testkit/testkit/Cargo.toml | 14 + .../testkit/testkit/src/bin/testkit-ready.rs | 29 +- tests/testkit/testkit/src/lib.rs | 12 + tests/testkit/testkit/src/program/capture.rs | 109 ++++ tests/testkit/testkit/src/program/junit.rs | 128 +++++ tests/testkit/testkit/src/program/mod.rs | 383 +++++++++++++ tests/testkit/testkit/src/program/process.rs | 216 +++++++ .../testkit/testkit/src/program/supervisor.rs | 334 +++++++++++ tests/testkit/testkit/src/ready.rs | 36 ++ tests/testkit/testkit/src/registry.rs | 208 +++++++ tests/testkit/testkit/tests/attribute.rs | 116 ++++ tests/testkit/testkit/tests/program.rs | 527 ++++++++++++++++++ 44 files changed, 3670 insertions(+), 86 deletions(-) create mode 100755 tasks/scripts/check-conformance-program.sh rename tests/suites/conformance/{tests => src}/cli.rs (100%) rename tests/suites/conformance/{tests => src}/file_transfer.rs (100%) create mode 100644 tests/suites/conformance/src/lib.rs rename tests/suites/conformance/{tests => src}/lifecycle.rs (100%) create mode 100644 tests/suites/conformance/src/main.rs rename tests/suites/conformance/{tests => src}/persistent_workspace.rs (100%) rename tests/suites/conformance/{tests => src}/policy_advisor.rs (100%) rename tests/suites/conformance/{tests => src}/smoke.rs (100%) create mode 100644 tests/testkit/testkit/src/program/capture.rs create mode 100644 tests/testkit/testkit/src/program/junit.rs create mode 100644 tests/testkit/testkit/src/program/mod.rs create mode 100644 tests/testkit/testkit/src/program/process.rs create mode 100644 tests/testkit/testkit/src/program/supervisor.rs create mode 100644 tests/testkit/testkit/src/ready.rs create mode 100644 tests/testkit/testkit/src/registry.rs create mode 100644 tests/testkit/testkit/tests/attribute.rs create mode 100644 tests/testkit/testkit/tests/program.rs diff --git a/.github/actions/setup-e2e-cli/action.yml b/.github/actions/setup-e2e-cli/action.yml index 8b30970d8d..6bc2a78ec2 100644 --- a/.github/actions/setup-e2e-cli/action.yml +++ b/.github/actions/setup-e2e-cli/action.yml @@ -1,6 +1,12 @@ name: Setup E2E CLI description: Download architecture-matched prebuilt OpenShell host CLIs for E2E tests +inputs: + conformance: + description: Also download the prebuilt openshell-conformance program and set OPENSHELL_CONFORMANCE_BIN + required: false + default: "false" + runs: using: composite steps: @@ -17,3 +23,19 @@ runs: chmod +x "$cli" "$cli" --version echo "OPENSHELL_BIN=$cli" >> "$GITHUB_ENV" + + - name: Download prebuilt openshell-conformance + if: inputs.conformance == 'true' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ runner.arch == 'X64' && 'openshell-conformance-x86_64-unknown-linux-musl' || 'openshell-conformance-aarch64-unknown-linux-musl' }} + path: .e2e/prebuilt-conformance + + - name: Configure prebuilt openshell-conformance + if: inputs.conformance == 'true' + shell: bash + run: | # zizmor: ignore[github-env] persists only a fixed path under the trusted workspace + program="$GITHUB_WORKSPACE/.e2e/prebuilt-conformance/openshell-conformance" + chmod +x "$program" + "$program" list + echo "OPENSHELL_CONFORMANCE_BIN=$program" >> "$GITHUB_ENV" diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 8e21eb7a69..db22a74913 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -392,6 +392,7 @@ jobs: cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/tmachine/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/runner/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/support/Cargo.toml + tasks/scripts/check-conformance-program.sh rust-postgres-test: name: Rust PostgreSQL tests diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index cc558fb4e2..0f59ab2a81 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -144,6 +144,60 @@ jobs: cargo-version: ${{ needs.version.outputs.cargo }} extra-cargo-flags: --no-default-features + # The conformance program, built once per architecture for the host lanes. + # It lives in its own workspace and is not released, so it is not part of + # build-binaries.yml. + build-conformance: + name: openshell-conformance (${{ matrix.triple }}) + needs: [pr_metadata] + if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_any_e2e == 'true' + permissions: + contents: read + strategy: + matrix: + include: + - triple: x86_64-unknown-linux-musl + runner: linux-amd64-cpu8 + - triple: aarch64-unknown-linux-musl + runner: linux-arm64-cpu8 + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.sha }} + - uses: ./.github/actions/setup-nix + with: + prepare-shell: "true" + cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + - uses: ./.github/actions/setup-rust + with: + cache-key: binaries-v2-openshell-conformance-${{ matrix.triple }} + save-cache: ${{ github.ref == 'refs/heads/main' }} + - name: Build openshell-conformance + shell: nix develop -c bash -euo pipefail {0} + env: + # Build into the root target directory, which setup-rust caches. + CARGO_TARGET_DIR: ${{ github.workspace }}/target + TRIPLE: ${{ matrix.triple }} + run: | + cargo build --release --locked --target "$TRIPLE" \ + --manifest-path tests/suites/conformance/Cargo.toml \ + --bin openshell-conformance + binary="target/$TRIPLE/release/openshell-conformance" + # The program must run on any lane host. + tasks/scripts/verify-static-binary.sh "$binary" + # Fails if the program has no tests linked. + "$binary" list + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openshell-conformance-${{ matrix.triple }} + path: target/${{ matrix.triple }}/release/openshell-conformance + compression-level: 0 + retention-days: 5 + if-no-files-found: error + build-external-drivers: name: ${{ matrix.binary }} (${{ matrix.triple }}) needs: [pr_metadata, version] @@ -306,7 +360,7 @@ jobs: ] docker-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -314,11 +368,12 @@ jobs: packages: read uses: ./.github/workflows/e2e-docker-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} runner: linux-arm64-cpu8 podman-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -326,12 +381,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} suite-matrix: >- [{"suite":"ci","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:podman:ci"}] podman-additional-ca-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -339,21 +395,24 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} suite-matrix: >- [{"suite":"additional-ca","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"OPENSHELL_E2E_PODMAN_TEST=additional_ca mise run --no-deps --skip-deps e2e:podman"}] vm-e2e: - needs: [pr_metadata, build-binaries, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read contents: read packages: read uses: ./.github/workflows/e2e-vm-test.yml + with: + prebuilt-conformance: true vm-additional-ca-e2e: - needs: [pr_metadata, build-binaries, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -361,11 +420,12 @@ jobs: packages: read uses: ./.github/workflows/e2e-vm-test.yml with: + prebuilt-conformance: true suite-name: additional-ca test-name: additional_ca docker-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -373,6 +433,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-docker-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} runner: linux-arm64-cpu8 gateway-artifact: openshell-gateway-plain-aarch64-unknown-linux-gnu @@ -381,7 +442,7 @@ jobs: [{"suite":"external-driver","cmd":"mise run --no-deps --skip-deps e2e:docker:external-driver","apt_packages":"openssh-client","python_proto":false,"mcp":false}] podman-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -389,6 +450,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-podman-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} gateway-artifact: openshell-gateway-plain-x86_64-unknown-linux-gnu external-driver-binary: openshell-driver-podman @@ -396,7 +458,7 @@ jobs: [{"suite":"external-driver","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:podman:external-driver"}] vm-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-vm-driver] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-vm-driver] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -404,6 +466,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-vm-test.yml with: + prebuilt-conformance: true gateway-artifact: openshell-gateway-plain-x86_64-unknown-linux-gnu suite-name: external-driver e2e-task: e2e:vm:external-driver @@ -420,7 +483,7 @@ jobs: image-tag: ${{ github.sha }} kubernetes-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' strategy: fail-fast: false @@ -444,13 +507,14 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: ${{ matrix.job_name }} agent-sandbox-version: ${{ matrix.agent_sandbox_version }} test-name: ${{ matrix.test_name }} kubernetes-app-listener-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' strategy: fail-fast: false @@ -478,6 +542,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes app listener (${{ matrix.name }}) e2e-task: e2e:kubernetes:app-listener @@ -487,7 +552,7 @@ jobs: listener-takeover: ${{ matrix.takeover }} kubernetes-workspace-managed-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -495,12 +560,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (workspace managed mode) e2e-task: e2e:kubernetes:workspace-managed kubernetes-external-driver-e2e: - needs: [pr_metadata, build-binaries, build-gateway-plain, build-external-drivers, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-gateway-plain, build-external-drivers, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -508,6 +574,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (external compute driver) e2e-task: e2e:kubernetes:external-driver @@ -516,7 +583,7 @@ jobs: cluster-images: sandbox supervisor kubernetes-workspace-operator-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true' permissions: actions: read @@ -524,12 +591,13 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes E2E (workspace operator mode) e2e-task: e2e:kubernetes:workspace-operator kubernetes-ha-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_ha_e2e == 'true' permissions: actions: read @@ -537,6 +605,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes HA E2E (Rust smoke) extra-helm-values: deploy/helm/openshell/ci/values-high-availability.yaml @@ -546,7 +615,7 @@ jobs: use-envoy-gateway: true kubernetes-credential-drivers-e2e: - needs: [pr_metadata, build-binaries, build-images] + needs: [pr_metadata, build-binaries, build-conformance, build-images] if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_kubernetes_credential_drivers_e2e == 'true' permissions: actions: read @@ -554,6 +623,7 @@ jobs: packages: read uses: ./.github/workflows/e2e-kubernetes-test.yml with: + prebuilt-conformance: true image-tag: ${{ github.sha }} job-name: Kubernetes Credential Drivers E2E e2e-task: e2e:kubernetes:credential-drivers diff --git a/.github/workflows/e2e-docker-test.yml b/.github/workflows/e2e-docker-test.yml index 501ded8d34..faa0121cd2 100644 --- a/.github/workflows/e2e-docker-test.yml +++ b/.github/workflows/e2e-docker-test.yml @@ -21,6 +21,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false external-driver-binary: required: false type: string @@ -67,6 +71,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/.github/workflows/e2e-kubernetes-test.yml b/.github/workflows/e2e-kubernetes-test.yml index 867b8c1dd7..1ae616e36e 100644 --- a/.github/workflows/e2e-kubernetes-test.yml +++ b/.github/workflows/e2e-kubernetes-test.yml @@ -67,6 +67,11 @@ on: required: false type: string default: "" + prebuilt-conformance: + description: "Run conformance with the openshell-conformance artifact instead of building it" + required: false + type: boolean + default: false external-driver-binary: description: "Optional standalone compute driver binary used to compose a local test image" required: false @@ -124,6 +129,8 @@ jobs: - name: Use prebuilt OpenShell CLI uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - name: Use prebuilt OpenShell gateway if: inputs.gateway-artifact != '' diff --git a/.github/workflows/e2e-podman-test.yml b/.github/workflows/e2e-podman-test.yml index d70ad6c60c..ac6d7d66ef 100644 --- a/.github/workflows/e2e-podman-test.yml +++ b/.github/workflows/e2e-podman-test.yml @@ -17,6 +17,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false external-driver-binary: required: false type: string @@ -52,6 +56,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/.github/workflows/e2e-vm-test.yml b/.github/workflows/e2e-vm-test.yml index 28b4c81db5..82d86058b8 100644 --- a/.github/workflows/e2e-vm-test.yml +++ b/.github/workflows/e2e-vm-test.yml @@ -14,6 +14,10 @@ on: required: false type: string default: "" + prebuilt-conformance: + required: false + type: boolean + default: false suite-name: required: false type: string @@ -47,6 +51,8 @@ jobs: persist-credentials: false - uses: ./.github/actions/setup-e2e-cli + with: + conformance: ${{ inputs.prebuilt-conformance }} - uses: ./.github/actions/setup-e2e-gateway with: diff --git a/CI.md b/CI.md index ff278592e7..a1a1b8bc77 100644 --- a/CI.md +++ b/CI.md @@ -218,7 +218,7 @@ nextest filterset: nix run .#openshell-test-runner -- \ --environment ubuntu-docker-rootful --installer deb \ --family conformance --package conformance \ - --filter 'binary_id(=conformance::policy_advisor)' + --filter 'test(/^policy_advisor::/)' ``` A local run does not replace the required PR E2E gate. diff --git a/TESTING.md b/TESTING.md index 4d0213ef90..12d81ee984 100644 --- a/TESTING.md +++ b/TESTING.md @@ -181,6 +181,12 @@ by `OPENSHELL_BIN`. See [tests/testkit/README.md](tests/testkit/README.md) for how to write and run tests. +The conformance crate also builds `openshell-conformance`, a program that +runs the conformance suite against a gateway without cargo or nextest +(`openshell-conformance list`, `ready`, `run`). It covers only the +conformance suite. See +[tests/suites/conformance/README.md](tests/suites/conformance/README.md). + Suites live under `tests/suites/`, in Cargo workspaces with their own lockfiles: `conformance`, `features`, and one workspace per driver suite under `drivers/`. Run the `conformance` suites against a local Docker @@ -419,7 +425,10 @@ Suites: - Common suite (`--features e2e`) - driver-neutral CLI behavior, sandbox lifecycle, sync, port forwarding, policy, and provider tests. - Conformance (`tests/suites/conformance`) - the testkit `conformance` crate: lifecycle, the CLI, persistent workspaces, file transfer, and the policy - advisor. Driver E2E runs it with nextest against the lane's gateway. The + advisor. Driver E2E runs it with `openshell-conformance` against the lane's + gateway and writes a JUnit report under `results/`; set + `OPENSHELL_CONFORMANCE_BIN` to use a prebuilt program, as Branch E2E does + with the static build it makes once per architecture. The installed-artifact conformance lanes run the same crate through `openshell-test-runner` (see above). - Driver suites (`--features e2e-docker`, `e2e-podman`, `e2e-kubernetes`, or diff --git a/e2e/policy-advisor/README.md b/e2e/policy-advisor/README.md index c5c4e79ca0..adb700306f 100644 --- a/e2e/policy-advisor/README.md +++ b/e2e/policy-advisor/README.md @@ -69,7 +69,7 @@ test runner: nix run .#openshell-test-runner -- \ --environment ubuntu-docker-rootful --installer deb \ --family conformance --package conformance \ - --filter 'binary_id(=conformance::policy_advisor)' + --filter 'test(/^policy_advisor::/)' ``` The GitHub write test above and the regressions below still exercise distinct diff --git a/e2e/rust/e2e-kubernetes.sh b/e2e/rust/e2e-kubernetes.sh index 9bdcf4e6a2..fc90a827ee 100755 --- a/e2e/rust/e2e-kubernetes.sh +++ b/e2e/rust/e2e-kubernetes.sh @@ -79,7 +79,8 @@ run_conformance() { return 0 fi - e2e_run_openshell_conformance "Kubernetes" + OPENSHELL_CONFORMANCE_JUNIT="${OPENSHELL_CONFORMANCE_JUNIT:-${ROOT}/results/${OPENSHELL_E2E_REPORT_NAME:-e2e-kubernetes}-conformance.xml}" \ + e2e_run_openshell_conformance "Kubernetes" } # `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`) names the report diff --git a/e2e/support/conformance.sh b/e2e/support/conformance.sh index 26cb3ed9f6..56ce5a50f1 100644 --- a/e2e/support/conformance.sh +++ b/e2e/support/conformance.sh @@ -16,30 +16,62 @@ e2e_require_openshell_bin() { return 2 fi - # Cargo runs each test from its package directory, so resolve the candidate - # relative to the caller's working directory before passing it to the tests. + # Tests may run from another directory, so resolve the candidate relative + # to the caller's working directory before passing it to them. local binary_dir binary_dir="$(cd "$(dirname "${OPENSHELL_BIN}")" && pwd -P)" || return export OPENSHELL_BIN="${binary_dir}/$(basename "${OPENSHELL_BIN}")" } +# Print the path of the openshell-conformance program: OPENSHELL_CONFORMANCE_BIN +# if set, else a debug build from this checkout. +e2e_openshell_conformance_bin() { + local root=$1 + + if [ -n "${OPENSHELL_CONFORMANCE_BIN:-}" ]; then + if [ ! -x "${OPENSHELL_CONFORMANCE_BIN}" ]; then + echo "ERROR: openshell-conformance is not executable: ${OPENSHELL_CONFORMANCE_BIN}" >&2 + return 2 + fi + echo "${OPENSHELL_CONFORMANCE_BIN}" + return + fi + + # Cargo reports where it put the binary, which depends on the target + # directory and on CARGO_BUILD_TARGET. Diagnostics still go to stderr. + local messages program + messages="$(cargo build \ + --locked \ + --message-format=json-render-diagnostics \ + --manifest-path "${root}/tests/suites/conformance/Cargo.toml" \ + --bin openshell-conformance)" || return + program="$(printf '%s\n' "${messages}" \ + | sed -n 's/.*"executable":"\([^"]*\)".*/\1/p' | tail -n 1)" + if [ -z "${program}" ] || [ ! -x "${program}" ]; then + echo "ERROR: cargo did not report the openshell-conformance binary" >&2 + return 2 + fi + echo "${program}" +} + +# Run the conformance suite with openshell-conformance. The JUnit report goes +# to OPENSHELL_CONFORMANCE_JUNIT, else results/conformance-.xml. e2e_run_openshell_conformance() { local gateway_label=${1:-OpenShell} - local root + local root program junit root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" e2e_require_openshell_bin || return + program="$(e2e_openshell_conformance_bin "${root}")" || return + junit="${OPENSHELL_CONFORMANCE_JUNIT:-${root}/results/conformance-$(echo "${gateway_label}" | tr '[:upper:]' '[:lower:]').xml}" + mkdir -p "$(dirname "${junit}")" || return - # The tests find the gateway as the CLI does: OPENSHELL_GATEWAY_ENDPOINT, + # The program finds the gateway as the CLI does: OPENSHELL_GATEWAY_ENDPOINT, # else OPENSHELL_GATEWAY, else the active gateway. The gateway wrappers set # these for the gateway they started. echo "==> Running conformance suites against the ${gateway_label} gateway" - cargo nextest run \ - --locked \ - --manifest-path "${root}/tests/suites/conformance/Cargo.toml" \ - --test-threads 1 \ - --no-fail-fast \ - --no-tests fail + "${program}" ready || return + "${program}" run --junit "${junit}" } if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then diff --git a/tasks/scripts/check-conformance-program.sh b/tasks/scripts/check-conformance-program.sh new file mode 100755 index 0000000000..7bfb2ac8c4 --- /dev/null +++ b/tasks/scripts/check-conformance-program.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Check that `openshell-conformance list` names the same tests that nextest +# lists for the conformance crate, so the program runs the whole suite. +# Neither command needs a gateway. + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +manifest="${root}/tests/suites/conformance/Cargo.toml" +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT + +cargo run --quiet --locked --manifest-path "${manifest}" --bin openshell-conformance -- list \ + | awk '{print $1}' | sort > "${work}/program" +cargo nextest list --locked --color never --manifest-path "${manifest}" --message-format oneline \ + | awk '{print $2}' | sort > "${work}/nextest" + +if [[ ! -s "${work}/nextest" ]]; then + echo "error: could not read test names from nextest's list; has its oneline format changed?" >&2 + exit 1 +fi +if ! diff -u "${work}/nextest" "${work}/program"; then + echo "error: openshell-conformance list differs from nextest's list (-nextest +program)" >&2 + exit 1 +fi +if [[ ! -s "${work}/program" ]]; then + echo "error: openshell-conformance lists no tests" >&2 + exit 1 +fi +echo "openshell-conformance lists the same $(wc -l < "${work}/program" | tr -d ' ') tests as nextest" diff --git a/tasks/test.toml b/tasks/test.toml index 4309ad95b7..2839c1cf0c 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -134,6 +134,7 @@ run = [ "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/tmachine/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/runner/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/support/Cargo.toml", + "tasks/scripts/check-conformance-program.sh", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-precommit native" hide = true diff --git a/tests/runner/src/main.rs b/tests/runner/src/main.rs index 1503a4e0d7..9bac397824 100644 --- a/tests/runner/src/main.rs +++ b/tests/runner/src/main.rs @@ -38,7 +38,7 @@ struct Cli { #[arg(long = "package", short = 'p', value_name = "PACKAGE")] packages: Vec, /// A nextest filterset narrowing the tests that run, e.g. - /// `binary(smoke)`. + /// `test(/^smoke::/)`. #[arg(long, value_name = "FILTERSET")] filter: Option, /// The gateway name the installer registers with the CLI. The packaged diff --git a/tests/suites/conformance/Cargo.lock b/tests/suites/conformance/Cargo.lock index f45f1866ee..ea04cd150d 100644 --- a/tests/suites/conformance/Cargo.lock +++ b/tests/suites/conformance/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -679,6 +775,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1395,6 +1497,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1641,6 +1749,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1758,6 +1898,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1873,6 +2022,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1995,11 +2150,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2357,6 +2517,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3134,6 +3318,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3720,6 +3913,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3743,6 +3942,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/suites/conformance/Cargo.toml b/tests/suites/conformance/Cargo.toml index 40541d88c4..99274d8dfa 100644 --- a/tests/suites/conformance/Cargo.toml +++ b/tests/suites/conformance/Cargo.toml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# The conformance family is one crate; each file in `tests/` is a suite. +# The conformance family is one crate; each module in `src/` is a suite. [package] name = "conformance" description = "Conformance suite: sandbox behavior on every compute driver" @@ -12,6 +12,14 @@ license = "Apache-2.0" repository = "https://github.com/NVIDIA/OpenShell" publish = false +# `openshell-conformance`: the suite as one program to run against a gateway. +# No test target of its own: cargo test and nextest run the tests through the +# library. +[[bin]] +name = "openshell-conformance" +path = "src/main.rs" +test = false + [dependencies] openshell-testkit = { path = "../../testkit/testkit" } serde_json = "1" diff --git a/tests/suites/conformance/README.md b/tests/suites/conformance/README.md index ee0502d072..fe5c95b5f8 100644 --- a/tests/suites/conformance/README.md +++ b/tests/suites/conformance/README.md @@ -2,7 +2,9 @@ The `conformance` crate holds the testkit suites for sandbox behavior that every compute driver must provide. The family is this one crate, its own workspace. -Each file in `tests/` is one suite. +It is a library: each module in `src/` is one suite, and `cargo test` and +nextest run the tests as the library's unit tests. The crate also builds the +`openshell-conformance` program, which runs the same tests. | Suite | Covers | |---|---| @@ -26,11 +28,30 @@ environments). [TESTING.md](../../../TESTING.md) covers running it against a local Docker gateway and in a tmachine VM, as CI does. +### The `openshell-conformance` program + +`openshell-conformance` runs the suite against a gateway without cargo or +nextest, so a driver author or operator can check their own gateway. It finds +the gateway and the CLI as the tests do. Build it and run it with: + +```shell +cargo build --locked --manifest-path tests/suites/conformance/Cargo.toml --bin openshell-conformance +tests/suites/conformance/target/debug/openshell-conformance list +tests/suites/conformance/target/debug/openshell-conformance ready +tests/suites/conformance/target/debug/openshell-conformance run --junit conformance.xml +``` + +The program runs only this suite: passing it means a gateway meets the +conformance contract below. Feature and driver suites are not part of it. The +[testkit README](../../testkit/README.md#test-programs) describes the commands +and exit codes. `tasks/scripts/check-conformance-program.sh` checks that it +lists the same tests as nextest. + ## Adding a test -Add a `#[testkit::test]` function to a suite, or a new file in `tests/` for a new -area. No playbook, test archive or workflow entry is needed: the runner builds -and runs every test in the crate. +Add a `#[testkit::test]` function to a suite, or for a new area, a new file in +`src/` and a `mod` line for it in `src/lib.rs`. `openshell-conformance` +includes every test in the crate. ## What a test may assert diff --git a/tests/suites/conformance/tests/cli.rs b/tests/suites/conformance/src/cli.rs similarity index 100% rename from tests/suites/conformance/tests/cli.rs rename to tests/suites/conformance/src/cli.rs diff --git a/tests/suites/conformance/tests/file_transfer.rs b/tests/suites/conformance/src/file_transfer.rs similarity index 100% rename from tests/suites/conformance/tests/file_transfer.rs rename to tests/suites/conformance/src/file_transfer.rs diff --git a/tests/suites/conformance/src/lib.rs b/tests/suites/conformance/src/lib.rs new file mode 100644 index 0000000000..00a4d45027 --- /dev/null +++ b/tests/suites/conformance/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Conformance suite: sandbox behavior that every compute driver must +//! provide. Each module is one suite. +//! +//! `cargo test` and nextest run the tests as this library's unit tests. +//! `#[testkit::test]` also registers them, so the `openshell-conformance` +//! program in `src/main.rs` runs them by linking this library. + +mod cli; +mod file_transfer; +mod lifecycle; +mod persistent_workspace; +mod policy_advisor; +mod smoke; diff --git a/tests/suites/conformance/tests/lifecycle.rs b/tests/suites/conformance/src/lifecycle.rs similarity index 100% rename from tests/suites/conformance/tests/lifecycle.rs rename to tests/suites/conformance/src/lifecycle.rs diff --git a/tests/suites/conformance/src/main.rs b/tests/suites/conformance/src/main.rs new file mode 100644 index 0000000000..7b4e899c60 --- /dev/null +++ b/tests/suites/conformance/src/main.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `openshell-conformance`: the conformance suite as one program, to run +//! against a gateway without cargo or nextest. +//! +//! The tests are registered by `#[testkit::test]` in this crate's library; +//! see `testkit::program` for the commands. + +use std::process::ExitCode; + +// A dependency that is never named is not linked, and its tests would be +// missing. +use conformance as _; + +fn main() -> ExitCode { + testkit::main() +} diff --git a/tests/suites/conformance/tests/persistent_workspace.rs b/tests/suites/conformance/src/persistent_workspace.rs similarity index 100% rename from tests/suites/conformance/tests/persistent_workspace.rs rename to tests/suites/conformance/src/persistent_workspace.rs diff --git a/tests/suites/conformance/tests/policy_advisor.rs b/tests/suites/conformance/src/policy_advisor.rs similarity index 100% rename from tests/suites/conformance/tests/policy_advisor.rs rename to tests/suites/conformance/src/policy_advisor.rs diff --git a/tests/suites/conformance/tests/smoke.rs b/tests/suites/conformance/src/smoke.rs similarity index 100% rename from tests/suites/conformance/tests/smoke.rs rename to tests/suites/conformance/src/smoke.rs diff --git a/tests/suites/drivers/podman/Cargo.lock b/tests/suites/drivers/podman/Cargo.lock index 452ee563cf..6633a3e703 100644 --- a/tests/suites/drivers/podman/Cargo.lock +++ b/tests/suites/drivers/podman/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1394,6 +1496,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1640,6 +1748,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1757,6 +1897,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1872,6 +2021,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1994,11 +2149,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2373,6 +2533,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3150,6 +3334,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3736,6 +3929,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3759,6 +3958,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index 4362c9cacd..c12c38f5ce 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1394,6 +1496,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1658,6 +1766,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1775,6 +1915,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1900,6 +2049,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -2034,11 +2189,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2396,6 +2556,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3173,6 +3357,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3759,6 +3952,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3782,6 +3981,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/support/Cargo.lock b/tests/support/Cargo.lock index a7e717b011..f8b591438f 100644 --- a/tests/support/Cargo.lock +++ b/tests/support/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -678,6 +774,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1394,6 +1496,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1649,6 +1757,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1766,6 +1906,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1881,6 +2030,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -2003,11 +2158,16 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", @@ -2373,6 +2533,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3150,6 +3334,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3736,6 +3929,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3759,6 +3958,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/testkit/Cargo.lock b/tests/testkit/Cargo.lock index 01973c00e4..db2e79b6ca 100644 --- a/tests/testkit/Cargo.lock +++ b/tests/testkit/Cargo.lock @@ -54,6 +54,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.104" @@ -382,6 +432,46 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + [[package]] name = "cmake" version = "0.1.58" @@ -397,6 +487,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -669,6 +765,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "etcetera" version = "0.11.0" @@ -1385,6 +1487,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.14.0" @@ -1631,6 +1739,38 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3045e122bd98aef8ec3ad58ce84f0791f64e70163d1a02710af4aa11a4d54cc5" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77060ebe535362c3da75682cd17b0431017b6e7c5661e714fc69a7ad017d1301" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1748,6 +1888,15 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "newtype-uuid" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa56da58097c7ae893c4097bb6d91bb5f7fa330053e07e033349db4d5f83ba8" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" @@ -1863,6 +2012,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "openshell-bootstrap" version = "0.0.0" @@ -1985,14 +2140,20 @@ dependencies = [ name = "openshell-testkit" version = "0.0.0" dependencies = [ + "clap", "futures", + "libc", + "libtest-mimic", + "linkme", "openshell-bootstrap", "openshell-core", "openshell-sdk", "openshell-testkit-macros", + "quick-junit", "rand 0.9.4", "serde", "serde_json", + "tempfile", "tokio", "tonic", ] @@ -2348,6 +2509,30 @@ dependencies = [ "pulldown-cmark", ] +[[package]] +name = "quick-junit" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae23b8e1a59a90f7bf98caa2f4c8c2eedf6df011c6c88ed633e85a87c84526e" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.20", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -3125,6 +3310,15 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3711,6 +3905,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" @@ -3734,6 +3934,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + [[package]] name = "want" version = "0.3.1" diff --git a/tests/testkit/README.md b/tests/testkit/README.md index 1aea174049..3f786fe65d 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -7,7 +7,7 @@ This is a separate Cargo workspace with its own lockfile: | Crate | Purpose | |---|---| -| `testkit` | Finds and connects to the gateway, creates and cleans up sandboxes, runs the CLI. Also builds `testkit-ready`. | +| `testkit` | Finds and connects to the gateway, creates and cleans up sandboxes, runs the CLI, and runs test programs. Also builds `testkit-ready`. | | `testkit-macros` | The `#[testkit::test]` attribute. | ## Writing a test @@ -123,6 +123,67 @@ In a tmachine VM, the test runner (`tests/runner/`) does the same: it sets `OPENSHELL_BIN` to the installed CLI, then runs the suites with nextest. See [TESTING.md](../../TESTING.md). +## Test programs + +`#[testkit::test]` also registers the test with testkit. A binary whose +`main` calls `testkit::main()` runs every registered test it links, without +cargo or nextest: + +```rust +use conformance as _; + +fn main() -> std::process::ExitCode { + testkit::main() +} +``` + +The program must name each suite crate it runs, as `use conformance as _;` does +above: Rust doesn't link a dependency that is never named, and its tests would +be missing. A program that links no tests exits 2 with an error under every +command. `openshell-conformance`, a binary in the conformance crate, is the +program for the conformance suite; see +[its README](../suites/conformance/README.md). + +| Command | What it does | +|---|---| +| `list [FILTER]...` | Prints each test's name and description. Ignored tests are included and marked. | +| `ready [--timeout SECS]` | Waits for the gateway, like `testkit-ready` (default 60 seconds). Exits 2 if it is not ready in time. | +| `run [FILTER]...` | Runs the tests one at a time, each in its own process. Unix only. | + +A filter selects tests whose name contains it. `run` also takes: + +- `--include-ignored`: runs ignored tests too, instead of reporting them as + skipped. +- `--timeout SECS` (default 900): a test that runs longer gets SIGTERM, then + SIGKILL after `--grace SECS` (default 10). +- `--junit PATH`: writes a JUnit report with one test suite per family. + +Each test runs in its own process group. When the test's process exits, `run` +kills anything left in the group, such as CLI processes the test started. +Interrupting `run` stops the current test the same way and runs no more +tests; a second interrupt skips the grace period. If `run` itself is killed +with SIGKILL, the current test's process group keeps running. In a container +where `run` is PID 1, start it under an init, for example with +`docker run --init`. + +`run` exits 0 when every test passed, 1 when a test failed, timed out, or +crashed, and 2 when tests could not run: no tests matched, a test process +could not start or run its test, or the run was interrupted. + +A test that is stopped does not clean up: sandboxes it created stay on the +gateway. Run against a gateway you can throw away, or delete them by hand. + +Without a subcommand, the program takes libtest's arguments instead. A +`[[test]]` target with `harness = false` therefore also runs under `cargo test` +and nextest. In that mode tests run in one process, one at a time unless +`--test-threads` says otherwise. + +A test's name is its module path without the crate name, then its function +name, as in nextest. Its family, which groups tests in the JUnit report, is +the first directory under the last `suites` directory in the crate's path: +`conformance` for `tests/suites/conformance`, `features` for +`tests/suites/features/`. + ## Checks ```shell diff --git a/tests/testkit/testkit-macros/src/lib.rs b/tests/testkit/testkit-macros/src/lib.rs index fc3dbbdb68..b8a614f3c6 100644 --- a/tests/testkit/testkit-macros/src/lib.rs +++ b/tests/testkit/testkit-macros/src/lib.rs @@ -13,12 +13,28 @@ //! async fn expose_service(cx: &mut Context) -> testkit::Result { /* ... */ } //! ``` //! -//! The attribute expands to a `#[tokio::test]` wrapper with the same name, so -//! the nextest test name is the function name. The original function is nested -//! in the wrapper and passed to `testkit::run`. Other attributes, such as -//! `#[ignore]` or `#[cfg(...)]`, move to the wrapper. `#[should_panic]` is -//! rejected: a testkit test panics on any failure, so it would pass for the -//! wrong reason. Check the expected failure in the body instead. +//! The attribute generates three items: +//! +//! - a hidden `async fn` that runs the original function through +//! `testkit::run`, with the original function nested in it; +//! - under `#[cfg(test)]`, a `#[tokio::test]` wrapper with the same name that +//! calls it, so `cargo test` and nextest run the test like any other and the +//! test name is the function name; +//! - an entry in `testkit`'s test registry, with the test's family, name, +//! description, and `#[ignore]`, which `testkit::main()` runs. +//! +//! Other attributes, such as `#[ignore]` or `#[cfg(...)]`, move to the +//! wrapper. `#[cfg(...)]` also applies to the other two items, and lint +//! attributes (`allow`, `warn`, `deny`, `forbid`) also to the hidden +//! function. `#[should_panic]` is rejected: a testkit test panics on any +//! failure, so it would pass for the wrong reason. Check the expected failure +//! in the body instead. `#[cfg_attr]` and `#[expect]` are rejected too. +//! +//! A test's family is the first directory under `tests/suites/` in its +//! crate's path: `conformance` for `tests/suites/conformance`, `features` for +//! `tests/suites/features/`. + +use std::path::Path; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; @@ -27,12 +43,17 @@ use syn::{Attribute, Expr, ExprLit, ItemFn, Lit, LitStr, Meta}; #[proc_macro_attribute] pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream { - expand(attr.into(), item.into()) + let family = std::env::var_os("CARGO_MANIFEST_DIR").and_then(|dir| family_of(Path::new(&dir))); + expand(attr.into(), item.into(), family.as_deref()) .unwrap_or_else(syn::Error::into_compile_error) .into() } -fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { +fn expand( + attr: TokenStream2, + item: TokenStream2, + family: Option<&str>, +) -> syn::Result { let mut body: ItemFn = syn::parse2(item)?; if !attr.is_empty() { return Err(syn::Error::new_spanned( @@ -58,17 +79,24 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { "`#[testkit::test]` already makes this a test; remove this attribute", )); } - if let Some(attr) = body - .attrs - .iter() - .find(|attr| attr.path().is_ident("should_panic")) - { - return Err(syn::Error::new_spanned( - attr, + for attr in &body.attrs { + let message = if attr.path().is_ident("should_panic") { "testkit tests panic on any failure, so `#[should_panic]` would pass for the \ wrong reason; check the expected failure in the body, for example with \ - `CliOutput::failure` and `testkit::ensure!`", - )); + `CliOutput::failure` and `testkit::ensure!`" + } else if attr.path().is_ident("cfg_attr") { + // The registration reads `#[ignore]` and `#[cfg]` directly, so it + // would not see them inside `cfg_attr`. + "`#[cfg_attr]` is not supported on testkit tests; use `#[cfg]` or `#[ignore]` \ + directly" + } else if attr.path().is_ident("expect") { + // The lint attributes are copied to two functions, and only one + // of them would raise the lint. + "`#[expect]` is not supported on testkit tests; use `#[allow]`" + } else { + continue; + }; + return Err(syn::Error::new_spanned(attr, message)); } let description = doc_comment(&body.attrs); if description.is_empty() { @@ -77,20 +105,46 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { "testkit tests need a doc comment describing the behavior they verify", )); } + let ignore = ignore(&body.attrs)?; let name = body.sig.ident.clone(); let name_str = LitStr::new(&name.to_string(), name.span()); let description = LitStr::new(&description, name.span()); + let family = match family { + Some(family) => quote!(::core::option::Option::Some(#family)), + None => quote!(::core::option::Option::None), + }; + let (ignored, ignore_reason) = match ignore { + None => (false, quote!(::core::option::Option::None)), + Some(None) => (true, quote!(::core::option::Option::None)), + Some(Some(reason)) => (true, quote!(::core::option::Option::Some(#reason))), + }; let vis = body.vis.clone(); let attrs = std::mem::take(&mut body.attrs); + let cfgs: Vec<&Attribute> = attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .collect(); + // `#[cfg]` and lint attributes such as `#[allow(...)]` must reach the + // body, which is nested in the hidden function. Anything else is for the + // test itself, so goes on the wrapper only. + let body_attrs: Vec<&Attribute> = attrs + .iter() + .filter(|attr| { + ["cfg", "allow", "warn", "deny", "forbid"] + .iter() + .any(|name| attr.path().is_ident(name)) + }) + .collect(); body.vis = syn::Visibility::Inherited; body.sig.ident = format_ident!("body"); + let entry = format_ident!("__testkit_{}", name); Ok(quote! { - #(#attrs)* - #[::testkit::__private::tokio::test(crate = "::testkit::__private::tokio")] - #vis async fn #name() { + #(#body_attrs)* + #[doc(hidden)] + async fn #entry() { #body ::testkit::run( ::testkit::TestInfo { @@ -101,6 +155,30 @@ fn expand(attr: TokenStream2, item: TokenStream2) -> syn::Result { ) .await; } + + #(#attrs)* + #[cfg(test)] + #[::testkit::__private::tokio::test(crate = "::testkit::__private::tokio")] + #vis async fn #name() { + #entry().await; + } + + #(#cfgs)* + const _: () = { + #[::testkit::__private::linkme::distributed_slice(::testkit::__private::TESTS)] + #[linkme(crate = ::testkit::__private::linkme)] + static TEST: ::testkit::Test = ::testkit::Test { + info: ::testkit::TestInfo { + name: #name_str, + description: #description, + }, + family: #family, + module_path: ::core::module_path!(), + ignored: #ignored, + ignore_reason: #ignore_reason, + run: || ::std::boxed::Box::pin(#entry()), + }; + }; }) } @@ -112,6 +190,31 @@ fn is_test_attr(attr: &Attribute) -> bool { .is_some_and(|segment| segment.ident == "test") } +/// `None` if the test is not ignored, else the `#[ignore = "..."]` reason if +/// it has one. +fn ignore(attrs: &[Attribute]) -> syn::Result>> { + let Some(attr) = attrs.iter().find(|attr| attr.path().is_ident("ignore")) else { + return Ok(None); + }; + match &attr.meta { + Meta::Path(_) => Ok(Some(None)), + Meta::NameValue(meta) => match &meta.value { + Expr::Lit(ExprLit { + lit: Lit::Str(reason), + .. + }) => Ok(Some(Some(reason.clone()))), + value => Err(syn::Error::new_spanned( + value, + "the `#[ignore]` reason must be a string literal", + )), + }, + Meta::List(_) => Err(syn::Error::new_spanned( + attr, + "use `#[ignore]` or `#[ignore = \"reason\"]`", + )), + } +} + /// The doc comment, joined into one line. Empty if there is none. fn doc_comment(attrs: &[Attribute]) -> String { attrs @@ -131,21 +234,40 @@ fn doc_comment(attrs: &[Attribute]) -> String { .join(" ") } +/// The family of a suite crate in `manifest_dir`: the first directory under +/// the nearest enclosing `suites/`. +fn family_of(manifest_dir: &Path) -> Option { + let components: Vec<_> = manifest_dir + .components() + .filter_map(|component| component.as_os_str().to_str()) + .collect(); + let suites = components + .iter() + .rposition(|component| *component == "suites")?; + components + .get(suites + 1) + .map(|family| (*family).to_string()) +} + #[cfg(test)] mod tests { - use super::expand; + use std::path::Path; + + use super::{expand, family_of}; + + const OK_FN: &str = "async fn t(cx: &mut Context) -> testkit::Result { Ok(()) }"; // A suite only compiles correct tests, so it never reaches these errors. + // `testkit/tests/macro.rs` checks what correct tests expand to. #[test] fn rejects_misuse() { - let ok_fn = "async fn t(cx: &mut Context) -> testkit::Result { Ok(()) }"; for (attr, item, error) in [ ( "id = \"t-1\"", - format!("/// Doc.\n{ok_fn}"), + format!("/// Doc.\n{OK_FN}"), "takes no arguments", ), - ("", ok_fn.to_string(), "doc comment"), + ("", OK_FN.to_string(), "doc comment"), ( "", "/// Doc.\nfn t(cx: &mut Context) -> testkit::Result { Ok(()) }".to_string(), @@ -158,17 +280,53 @@ mod tests { ), ( "", - format!("/// Doc.\n#[tokio::test]\n{ok_fn}"), + format!("/// Doc.\n#[tokio::test]\n{OK_FN}"), "already makes this a test", ), ( "", - format!("/// Doc.\n#[should_panic]\n{ok_fn}"), + format!("/// Doc.\n#[should_panic]\n{OK_FN}"), "`#[should_panic]`", ), + ( + "", + format!("/// Doc.\n#[ignore(later)]\n{OK_FN}"), + "`#[ignore]` or", + ), + ( + "", + format!("/// Doc.\n#[ignore = 1]\n{OK_FN}"), + "string literal", + ), + ( + "", + format!("/// Doc.\n#[cfg_attr(unix, ignore)]\n{OK_FN}"), + "`#[cfg_attr]` is not supported", + ), + ( + "", + format!("/// Doc.\n#[expect(unused)]\n{OK_FN}"), + "`#[expect]` is not supported", + ), ] { - let err = expand(attr.parse().unwrap(), item.parse().unwrap()).unwrap_err(); + let err = expand(attr.parse().unwrap(), item.parse().unwrap(), None).unwrap_err(); assert!(err.to_string().contains(error), "{item}: {err}"); } } + + #[test] + fn family_is_the_directory_under_suites() { + for (dir, family) in [ + ("/repo/tests/suites/conformance", Some("conformance")), + ( + "/repo/tests/suites/features/provider-refresh/keycloak", + Some("features"), + ), + ("/a/suites/x/tests/suites/drivers/podman", Some("drivers")), + ("/repo/tests/testkit/testkit", None), + ("/repo/tests/suites", None), + ] { + assert_eq!(family_of(Path::new(dir)).as_deref(), family, "{dir}"); + } + } } diff --git a/tests/testkit/testkit/Cargo.toml b/tests/testkit/testkit/Cargo.toml index b8570fbe2c..47ff56c13c 100644 --- a/tests/testkit/testkit/Cargo.toml +++ b/tests/testkit/testkit/Cargo.toml @@ -19,11 +19,25 @@ openshell-bootstrap = { path = "../../../crates/openshell-bootstrap" } openshell-core = { path = "../../../crates/openshell-core", default-features = false } openshell-sdk = { path = "../../../crates/openshell-sdk" } openshell-testkit-macros = { path = "../testkit-macros" } +clap = { version = "4", features = ["derive"] } futures = "0.3" +libtest-mimic = "0.8" +linkme = "0.3" +quick-junit = "0.8" rand = "0.9" serde = "1" serde_json = "1" tokio = { version = "1.43", features = ["macros", "process", "rt", "time"] } +[target.'cfg(unix)'.dependencies] +libc = "0.2" + [dev-dependencies] +tempfile = "3" tonic = "0.14" + +# A test program, run by `cargo test` and nextest through `testkit::main`'s +# libtest interface, that checks the program's own subcommands. +[[test]] +name = "program" +harness = false diff --git a/tests/testkit/testkit/src/bin/testkit-ready.rs b/tests/testkit/testkit/src/bin/testkit-ready.rs index 4f41b3bed4..03db9d3547 100644 --- a/tests/testkit/testkit/src/bin/testkit-ready.rs +++ b/tests/testkit/testkit/src/bin/testkit-ready.rs @@ -6,13 +6,12 @@ //! Usage: `testkit-ready [TIMEOUT_SECS]` (default 60), with the gateway //! selected as for tests. Run it once per environment before the test //! binaries; exits non-zero with the last error if the gateway is not healthy -//! in time, or if the CLI does not reach it. +//! in time, or if the CLI does not reach it. A test program's `ready` +//! subcommand does the same. use std::process::ExitCode; use std::time::Duration; -use testkit::{Cli, Gateway}; - const DEFAULT_TIMEOUT_SECS: u64 = 60; const USAGE: &str = "usage: testkit-ready [TIMEOUT_SECS] (default 60)"; @@ -32,25 +31,9 @@ async fn main() -> ExitCode { } }, }; - let result = async { - let gateway = Gateway::from_env()?; - eprintln!("testkit-ready: {}", gateway.describe()); - let health = gateway.wait_ready(Duration::from_secs(timeout)).await?; - Cli::from_env(&gateway).check_connected().await?; - testkit::Result::Ok(health) - } - .await; - match result { - Ok(health) => { - eprintln!( - "testkit-ready: gateway {} is healthy and the CLI reaches it", - health.version - ); - ExitCode::SUCCESS - } - Err(error) => { - eprintln!("testkit-ready: {error}"); - ExitCode::FAILURE - } + if testkit::check_ready("testkit-ready", Duration::from_secs(timeout)).await { + ExitCode::SUCCESS + } else { + ExitCode::FAILURE } } diff --git a/tests/testkit/testkit/src/lib.rs b/tests/testkit/testkit/src/lib.rs index 10db7d0e33..dee03cac53 100644 --- a/tests/testkit/testkit/src/lib.rs +++ b/tests/testkit/testkit/src/lib.rs @@ -21,6 +21,10 @@ //! //! Tests find the gateway the same way the `openshell` CLI does (see //! [`gateway`]). +//! +//! `#[testkit::test]` also registers each test, so a binary that calls +//! [`main`] is a test program that lists, runs, and reports its tests without +//! Cargo (see [`program`]). mod cleanup; mod cli; @@ -29,6 +33,9 @@ mod error; pub mod gateway; mod image; mod poll; +pub mod program; +mod ready; +mod registry; mod run; pub use cleanup::CLEANUP_TIMEOUT; @@ -42,10 +49,15 @@ pub use gateway::Gateway; pub use image::{BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE, base_image}; pub use openshell_sdk as sdk; pub use poll::{Poll, poll_until}; +pub use program::main; +pub use ready::check as check_ready; +pub use registry::Test; pub use run::{TestInfo, run}; pub use testkit_macros::test; #[doc(hidden)] pub mod __private { + pub use crate::registry::TESTS; + pub use linkme; pub use tokio; } diff --git a/tests/testkit/testkit/src/program/capture.rs b/tests/testkit/testkit/src/program/capture.rs new file mode 100644 index 0000000000..329c22ac68 --- /dev/null +++ b/tests/testkit/testkit/src/program/capture.rs @@ -0,0 +1,109 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Copying a test's output to the console while keeping it for the report. + +use std::collections::VecDeque; +use std::io::{self, Read, Write}; +use std::sync::{Arc, Mutex, PoisonError}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +/// How much of each stream is kept for the report. Output beyond this is +/// still shown; the report keeps the end, where failures are. +const KEPT_BYTES: usize = 4 << 20; + +/// How long to wait for a stream to close after the test's process group is +/// gone. A process that left the group can hold it open. +const CLOSE_TIMEOUT: Duration = Duration::from_secs(5); + +/// One stream being copied. +pub struct Capture { + kept: Arc>, + thread: JoinHandle<()>, +} + +#[derive(Default)] +struct Kept { + bytes: VecDeque, + dropped: usize, +} + +impl Capture { + /// Copy `stream` to `console` on a thread, keeping a copy. + pub fn start( + mut stream: impl Read + Send + 'static, + mut console: impl Write + Send + 'static, + ) -> Self { + let kept = Arc::new(Mutex::new(Kept::default())); + let thread = std::thread::spawn({ + let kept = Arc::clone(&kept); + move || { + let mut buffer = [0; 8192]; + loop { + let read = match stream.read(&mut buffer) { + Ok(0) => return, + Ok(read) => read, + Err(error) if error.kind() == io::ErrorKind::Interrupted => continue, + Err(_) => return, + }; + let chunk = &buffer[..read]; + let _ = console.write_all(chunk).and_then(|()| console.flush()); + kept.lock() + .unwrap_or_else(PoisonError::into_inner) + .push(chunk); + } + } + }); + Self { kept, thread } + } + + /// The output kept, once the stream closes or after a short wait. + pub fn finish(self) -> Vec { + let deadline = Instant::now() + CLOSE_TIMEOUT; + while !self.thread.is_finished() && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + if self.thread.is_finished() { + let _ = self.thread.join(); + } + let kept = self.kept.lock().unwrap_or_else(PoisonError::into_inner); + let mut bytes = Vec::with_capacity(kept.bytes.len() + 64); + if kept.dropped > 0 { + bytes.extend(format!("[{} earlier bytes not kept]\n", kept.dropped).as_bytes()); + } + bytes.extend(&kept.bytes); + bytes + } +} + +impl Kept { + fn push(&mut self, chunk: &[u8]) { + self.bytes.extend(chunk); + let excess = self.bytes.len().saturating_sub(KEPT_BYTES); + self.bytes.drain(..excess); + self.dropped += excess; + } +} + +#[cfg(test)] +mod tests { + use super::{Capture, KEPT_BYTES, Kept}; + + #[test] + fn copies_and_keeps_the_stream() { + let capture = Capture::start(&b"one\ntwo\n"[..], std::io::sink()); + assert_eq!(capture.finish(), b"one\ntwo\n"); + } + + #[test] + fn keeps_the_end_of_long_output() { + let mut kept = Kept::default(); + kept.push(&vec![b'a'; KEPT_BYTES]); + kept.push(b"end"); + assert_eq!(kept.bytes.len(), KEPT_BYTES); + let tail: Vec = kept.bytes.iter().rev().take(4).rev().copied().collect(); + assert_eq!(tail, b"aend"); + assert_eq!(kept.dropped, 3); + } +} diff --git a/tests/testkit/testkit/src/program/junit.rs b/tests/testkit/testkit/src/program/junit.rs new file mode 100644 index 0000000000..0a1e66d648 --- /dev/null +++ b/tests/testkit/testkit/src/program/junit.rs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The `JUnit` report of a run. +//! +//! One `` per family. Each `` is named by the test name, +//! with the module path as its class name and the description and family as +//! properties. + +use std::collections::BTreeMap; +use std::path::Path; + +use quick_junit::{NonSuccessKind, Property, Report, TestCase, TestCaseStatus, TestSuite}; + +use super::supervisor::{Outcome, TestRun}; + +pub fn report(program: &str, runs: &[TestRun<'_>]) -> Report { + let mut suites: BTreeMap<&str, TestSuite> = BTreeMap::new(); + for run in runs { + let family = run.test.family(); + suites + .entry(family) + .or_insert_with(|| TestSuite::new(family)) + .add_test_case(test_case(run)); + } + let mut report = Report::new(program); + report.add_test_suites(suites.into_values()); + report +} + +fn test_case(run: &TestRun<'_>) -> TestCase { + let status = match &run.outcome { + Outcome::Passed => TestCaseStatus::success(), + Outcome::Skipped => { + let mut status = TestCaseStatus::skipped(); + status.set_message(match run.test.ignore_reason { + Some(reason) => format!("ignored: {reason}"), + None => "ignored".to_string(), + }); + status + } + Outcome::Failed(message) => non_success(NonSuccessKind::Failure, "failure", message), + Outcome::TimedOut(message) => non_success(NonSuccessKind::Failure, "timeout", message), + Outcome::Error(message) => non_success(NonSuccessKind::Error, "error", message), + Outcome::Interrupted => non_success(NonSuccessKind::Error, "interrupted", "interrupted"), + }; + let mut case = TestCase::new(run.test.name(), status); + case.set_classname(run.test.module_path) + .add_property(Property::new("family", run.test.family())) + .add_property(Property::new("description", run.test.info.description)); + if let Some(duration) = run.duration { + case.set_time(duration); + } + if !run.stdout.is_empty() { + case.set_system_out_lossy(&run.stdout); + } + if !run.stderr.is_empty() { + case.set_system_err_lossy(&run.stderr); + } + case +} + +fn non_success(kind: NonSuccessKind, ty: &str, message: &str) -> TestCaseStatus { + let mut status = TestCaseStatus::non_success(kind); + status.set_type(ty).set_message(message); + status +} + +pub fn write(path: &Path, report: &Report) -> std::io::Result<()> { + if let Some(parent) = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + { + std::fs::create_dir_all(parent)?; + } + let xml = report.to_string().map_err(std::io::Error::other)?; + std::fs::write(path, xml) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::report; + use crate::program::supervisor::{Outcome, TestRun}; + use crate::registry::Test; + use crate::registry::tests::test; + + const PASSED: Test = test("sandbox::smoke", "create_exec_delete", false); + const FAILED: Test = test("sandbox::lifecycle", "stop_start", false); + const SKIPPED: Test = Test { + ignore_reason: Some("https://github.com/NVIDIA/OpenShell/issues/1"), + ..test("sandbox::lifecycle", "later", true) + }; + + #[test] + fn reports_every_outcome() { + let run = |test, outcome, duration| TestRun { + test, + outcome, + duration, + stdout: b"out\n".to_vec(), + stderr: Vec::new(), + }; + let runs = [ + run(&PASSED, Outcome::Passed, Some(Duration::from_millis(1500))), + run( + &FAILED, + Outcome::TimedOut("timed out after 1s".into()), + Some(Duration::from_secs(1)), + ), + run(&SKIPPED, Outcome::Skipped, None), + ]; + let xml = report("openshell-conformance", &runs).to_string().unwrap(); + for expected in [ + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + r#""#, + "out\n", + ] { + assert!(xml.contains(expected), "missing {expected} in\n{xml}"); + } + } +} diff --git a/tests/testkit/testkit/src/program/mod.rs b/tests/testkit/testkit/src/program/mod.rs new file mode 100644 index 0000000000..c7ca98f3ba --- /dev/null +++ b/tests/testkit/testkit/src/program/mod.rs @@ -0,0 +1,383 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `testkit::main()`: a test program anyone can run against their gateway. +//! +//! A program is a binary whose `main` calls [`main`], linked with the crates +//! holding its `#[testkit::test]`s: +//! +//! ```ignore +//! fn main() -> std::process::ExitCode { +//! testkit::main() +//! } +//! ``` +//! +//! It has three subcommands: +//! +//! - `list [FILTER]...` prints each test's name and description. +//! - `ready [--timeout SECS]` waits for the gateway, as `testkit-ready` does. +//! - `run [FILTER]...` runs tests one at a time, each in its own process with +//! a timeout, and can write a `JUnit` report. Unix only. +//! +//! Any other arguments are libtest's, so the program also works as a +//! `harness = false` test target under `cargo test` and nextest: it answers +//! `--list --format terse`, `--ignored`, and ` --exact --nocapture`. +//! In that mode tests run in the program's own process, one at a time unless +//! `--test-threads` says otherwise. + +#[cfg(unix)] +mod capture; +#[cfg(unix)] +mod junit; +#[cfg(unix)] +mod process; +#[cfg(unix)] +mod supervisor; + +use std::ffi::OsString; +use std::path::PathBuf; +use std::process::ExitCode; +use std::time::Duration; + +use clap::{Parser, Subcommand}; + +use crate::registry::{RESERVED_NAMES, Selection, TESTS, Test, validate}; + +/// Exit code when a test failed. +const FAILED_EXIT: u8 = 1; +/// Exit code when tests could not run, including usage errors. +const ERROR_EXIT: u8 = 2; +/// Set by `run` for the test processes it starts. +const SUPERVISED_ENV: &str = "TESTKIT_SUPERVISED"; + +/// Run the program: the subcommands, or libtest's interface. +pub fn main() -> ExitCode { + main_with(&TESTS) +} + +/// [`main`] over `tests` instead of every registered test. +#[doc(hidden)] +pub fn main_with(tests: &'static [Test]) -> ExitCode { + let args: Vec = std::env::args_os().collect(); + let program = program_name(&args); + // A suite crate that the program never names is not linked, and its + // tests are silently missing. Fail instead of passing with nothing. + if tests.is_empty() { + eprintln!( + "{program}: no tests are linked into this program; it must name each suite crate \ + it runs, for example with `use as _;`" + ); + return ExitCode::from(ERROR_EXIT); + } + if let Err(error) = validate(tests) { + eprintln!("{program}: {error}"); + return ExitCode::from(ERROR_EXIT); + } + let first = args.get(1).and_then(|arg| arg.to_str()); + if first.is_some_and(|arg| RESERVED_NAMES.contains(&arg) || arg == "-h" || arg == "--help") { + let cli = match Cli::try_parse_from(&args) { + Ok(cli) => cli, + Err(error) => { + let _ = error.print(); + return ExitCode::from(if error.use_stderr() { ERROR_EXIT } else { 0 }); + } + }; + command(&program, tests, cli.command) + } else { + libtest(tests, args) + } +} + +#[derive(Parser)] +#[command( + about = "Run OpenShell tests against a gateway", + after_help = "The gateway is the one the openshell CLI selects: OPENSHELL_GATEWAY_ENDPOINT, \ + OPENSHELL_GATEWAY, or the active gateway. OPENSHELL_WORKSPACE selects the \ + workspace and OPENSHELL_BIN the CLI. Only plaintext and mTLS gateways are \ + supported.\n\nWithout a subcommand the program takes libtest's arguments, \ + as under cargo test and nextest." +)] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Print each test's name and description, including ignored tests. + List { + /// Show tests whose name contains any of these (all if none). + filters: Vec, + }, + /// Wait until the gateway is healthy and the CLI reaches it. Exits 2 if + /// it is not ready in time. + Ready { + /// How long to wait, in seconds. + #[arg(long, default_value = "60", value_name = "SECS", value_parser = seconds)] + timeout: Duration, + }, + /// Run tests one at a time, each in its own process. Exits 1 if a test + /// failed and 2 if tests could not run. + Run { + /// Run tests whose name contains any of these (all if none). + filters: Vec, + /// Run ignored tests too. + #[arg(long)] + include_ignored: bool, + /// Stop a test that runs longer than this, in seconds. Longer than + /// testkit's own step timeouts, so those report first. + #[arg(long, default_value = "900", value_name = "SECS", value_parser = seconds)] + timeout: Duration, + /// How long a stopped test has between SIGTERM and SIGKILL, in seconds. + #[arg(long, default_value = "10", value_name = "SECS", value_parser = seconds)] + grace: Duration, + /// Write a JUnit report here. + #[arg(long, value_name = "PATH")] + junit: Option, + }, +} + +/// A positive, finite number of seconds. +fn seconds(value: &str) -> Result { + let seconds: f64 = value + .parse() + .map_err(|_| format!("{value:?} is not a number"))?; + Duration::try_from_secs_f64(seconds) + .ok() + .filter(|duration| !duration.is_zero()) + .ok_or_else(|| format!("{value:?} is not a positive number of seconds")) +} + +fn command(program: &str, tests: &'static [Test], command: Command) -> ExitCode { + match command { + Command::List { filters } => { + list(tests, filters); + ExitCode::SUCCESS + } + Command::Ready { timeout } => ready(program, timeout), + Command::Run { + filters, + include_ignored, + timeout, + grace, + junit, + } => run( + program, + tests, + Selection { + filters, + include_ignored, + }, + timeout, + grace, + junit, + ), + } +} + +#[cfg(unix)] +fn run( + program: &str, + tests: &'static [Test], + selection: Selection, + timeout: Duration, + grace: Duration, + junit: Option, +) -> ExitCode { + let exe = match std::env::current_exe() { + Ok(exe) => exe, + Err(error) => { + eprintln!("{program}: find this program's path: {error}"); + return ExitCode::from(ERROR_EXIT); + } + }; + let options = supervisor::Options { + selection, + timeout, + grace, + junit, + }; + supervisor::run(program, &exe, tests, &options) +} + +#[cfg(not(unix))] +fn run( + program: &str, + _tests: &'static [Test], + _selection: Selection, + _timeout: Duration, + _grace: Duration, + _junit: Option, +) -> ExitCode { + eprintln!( + "{program}: `run` is only supported on Unix; run the tests with cargo test or nextest" + ); + ExitCode::from(ERROR_EXIT) +} + +/// One line per test: name and description, in aligned columns. +/// An ignored test's description ends with `[ignored]` or its reason. +fn list(tests: &[Test], filters: Vec) { + let selection = Selection { + filters, + include_ignored: true, + }; + let rows: Vec<_> = selection + .apply(tests) + .into_iter() + .map(|(test, _)| (test.name(), describe(test))) + .collect(); + let name_width = rows.iter().map(|row| row.0.len()).max().unwrap_or(0); + for (name, description) in rows { + println!("{name:name_width$} {description}"); + } +} + +fn describe(test: &Test) -> String { + let description = test.info.description; + match (test.ignored, test.ignore_reason) { + (false, _) => description.to_string(), + (true, None) => format!("{description} [ignored]"), + (true, Some(reason)) => format!("{description} [ignored: {reason}]"), + } +} + +fn ready(program: &str, timeout: Duration) -> ExitCode { + let runtime = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { + Ok(runtime) => runtime, + Err(error) => { + eprintln!("{program}: start the async runtime: {error}"); + return ExitCode::from(ERROR_EXIT); + } + }; + // A gateway that is not ready means the tests cannot run. + if runtime.block_on(crate::ready::check(program, timeout)) { + ExitCode::SUCCESS + } else { + ExitCode::from(ERROR_EXIT) + } +} + +/// libtest's interface, for `cargo test` and nextest, and for `run`'s test +/// processes. +fn libtest(tests: &'static [Test], args: Vec) -> ExitCode { + let mut args = libtest_mimic::Arguments::from_iter(args); + // Tests share one gateway; run them one at a time unless asked otherwise. + if args.test_threads.is_none() { + args.test_threads = Some(1); + } + let mut tests: Vec<&'static Test> = tests.iter().collect(); + tests.sort_by_cached_key(|test| test.name()); + let trials = tests + .into_iter() + .map(|test| { + libtest_mimic::Trial::test(test.name(), move || { + run_in_process(test); + Ok(()) + }) + .with_ignored_flag(test.ignored) + }) + .collect(); + let conclusion = libtest_mimic::run(&args, trials); + // `run` starts this process for exactly one test. Running none would + // look like a pass. + if std::env::var_os(SUPERVISED_ENV).is_some() + && !args.list + && conclusion.num_passed + conclusion.num_failed != 1 + { + eprintln!( + "testkit: expected to run exactly one test, ran {}", + conclusion.num_passed + conclusion.num_failed + ); + return ExitCode::from(ERROR_EXIT); + } + conclusion.exit_code() +} + +/// Run one test on a fresh current-thread runtime, as `#[tokio::test]` does. +/// Panics to fail it. +fn run_in_process(test: &Test) { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("start the async runtime") + .block_on((test.run)()); +} + +fn program_name(args: &[OsString]) -> String { + args.first() + .map(PathBuf::from) + .and_then(|path| { + path.file_name() + .map(|name| name.to_string_lossy().into_owned()) + }) + .unwrap_or_else(|| "testkit".to_string()) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use clap::Parser; + + use super::{Cli, Command}; + + #[test] + fn cli_is_consistent() { + ::command().debug_assert(); + } + + #[test] + fn run_parses_selection_and_limits() { + let cli = Cli::try_parse_from([ + "p", + "run", + "smoke", + "life", + "--include-ignored", + "--timeout", + "1.5", + "--junit", + "out/junit.xml", + ]) + .unwrap(); + let Command::Run { + filters, + include_ignored, + timeout, + grace, + junit, + } = cli.command + else { + panic!("not run"); + }; + assert_eq!(filters, ["smoke", "life"]); + assert!(include_ignored); + assert_eq!(timeout, Duration::from_millis(1500)); + assert_eq!(grace, Duration::from_secs(10)); + assert_eq!(junit.unwrap().to_str(), Some("out/junit.xml")); + + let Command::Ready { timeout } = Cli::try_parse_from(["p", "ready"]).unwrap().command + else { + panic!("not ready"); + }; + assert_eq!(timeout, Duration::from_secs(60)); + } + + #[test] + fn rejects_bad_limits() { + for bad in ["0", "-1", "soon", "inf", "NaN"] { + assert!( + Cli::try_parse_from(["p", "run", "--timeout", bad]).is_err(), + "{bad}" + ); + assert!( + Cli::try_parse_from(["p", "ready", "--timeout", bad]).is_err(), + "{bad}" + ); + } + } +} diff --git a/tests/testkit/testkit/src/program/process.rs b/tests/testkit/testkit/src/program/process.rs new file mode 100644 index 0000000000..1959452953 --- /dev/null +++ b/tests/testkit/testkit/src/program/process.rs @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! One test process: started in its own process group, waited for with a +//! timeout, and stopped with `SIGTERM`, a grace period, then `SIGKILL`. +//! +//! The process group is killed whenever the test process ends, so a test's +//! helpers, such as CLI processes, cannot outlive it. + +use std::io; +use std::os::unix::process::CommandExt as _; +use std::process::{Child, Command, ExitStatus}; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{Duration, Instant}; + +const POLL_INTERVAL: Duration = Duration::from_millis(20); + +/// How the test process ended. +#[derive(Debug)] +pub enum Ended { + Exited(ExitStatus), + /// Stopped after the timeout. + TimedOut, + /// Stopped because the supervisor was asked to stop. + Interrupted, +} + +/// Start `command` in its own process group. +pub fn spawn(command: &mut Command) -> io::Result { + command.process_group(0).spawn() +} + +/// Wait for `child`, stopping it after `timeout` or once the supervisor has +/// been asked to stop (see [`install_stop_handlers`]). The child is reaped +/// and its group killed on every path, including errors. +pub fn wait(child: &mut Child, timeout: Duration, grace: Duration) -> io::Result { + let result = wait_or_stop(child, timeout, grace); + if result.is_err() { + let _ = reap(child); + } + result +} + +fn wait_or_stop(child: &mut Child, timeout: Duration, grace: Duration) -> io::Result { + let deadline = Instant::now() + timeout; + loop { + if has_exited(child)? { + return Ok(Ended::Exited(reap(child)?)); + } + if stop_requested() { + stop(child, grace)?; + return Ok(Ended::Interrupted); + } + if Instant::now() >= deadline { + stop(child, grace)?; + return Ok(Ended::TimedOut); + } + std::thread::sleep(POLL_INTERVAL); + } +} + +/// `SIGTERM` the group, wait up to `grace`, then `SIGKILL` it. A stop signal +/// during the grace period, or a second one in all, cuts it short. +fn stop(child: &mut Child, grace: Duration) -> io::Result { + signal_group(child, libc::SIGTERM); + let deadline = Instant::now() + grace; + let before = stop_requests(); + let cut_short = || { + let now = stop_requests(); + now > before || now >= 2 + }; + while Instant::now() < deadline && !cut_short() && !has_exited(child)? { + std::thread::sleep(POLL_INTERVAL); + } + reap(child) +} + +/// Kill whatever is left of the group, then collect the exit status. +fn reap(child: &mut Child) -> io::Result { + signal_group(child, libc::SIGKILL); + child.wait() +} + +static STOP_REQUESTS: AtomicUsize = AtomicUsize::new(0); + +fn stop_requests() -> usize { + STOP_REQUESTS.load(Ordering::SeqCst) +} + +/// Whether the supervisor has been asked to stop since it started. +pub fn stop_requested() -> bool { + stop_requests() > 0 +} + +fn pid(child: &Child) -> libc::pid_t { + libc::pid_t::try_from(child.id()).expect("pid fits in pid_t") +} + +/// Whether the child has exited, without reaping it. While it is unreaped +/// its PID, and so its process group ID, cannot be reused, so signalling the +/// group cannot reach an unrelated process. +#[cfg(target_os = "linux")] +fn has_exited(child: &mut Child) -> io::Result { + // SAFETY: `info` is a valid out-pointer, and `WNOWAIT` leaves the child + // for `Child::wait` to reap. + unsafe { + let mut info: libc::siginfo_t = std::mem::zeroed(); + let result = libc::waitid( + libc::P_PID, + child.id(), + &raw mut info, + libc::WEXITED | libc::WNOHANG | libc::WNOWAIT, + ); + if result == -1 { + return Err(io::Error::last_os_error()); + } + Ok(info.si_pid() != 0) + } +} + +/// Whether the child has exited, reaping it. `waitid` with `WNOWAIT` is not +/// reliable outside Linux (see golang/go#19314), so the group is killed just +/// after the leader is reaped. Its ID could in principle be reused in that +/// window, though a new process would have to get the same PID and lead a +/// group. +#[cfg(not(target_os = "linux"))] +fn has_exited(child: &mut Child) -> io::Result { + Ok(child.try_wait()?.is_some()) +} + +fn signal_group(child: &Child, signal: libc::c_int) { + // SAFETY: plain syscall to the child's own group (see `spawn`). ESRCH, an + // empty group, is fine. + unsafe { + libc::kill(-pid(child), signal); + } +} + +extern "C" fn on_stop(_signal: libc::c_int) { + STOP_REQUESTS.fetch_add(1, Ordering::SeqCst); +} + +/// Count `SIGINT`, `SIGTERM`, and `SIGHUP` as stop requests instead of dying. +/// Tests run in their own process group, so a Ctrl-C at the terminal reaches +/// only the supervisor, which must stop the test. +pub fn install_stop_handlers() -> io::Result<()> { + for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGHUP] { + // SAFETY: the handler only touches an atomic, which is + // async-signal-safe. + unsafe { + let mut action: libc::sigaction = std::mem::zeroed(); + action.sa_sigaction = on_stop as *const () as libc::sighandler_t; + action.sa_flags = libc::SA_RESTART; + libc::sigemptyset(&raw mut action.sa_mask); + if libc::sigaction(signal, &raw const action, std::ptr::null_mut()) == -1 { + return Err(io::Error::last_os_error()); + } + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use std::process::{Command, Stdio}; + use std::time::{Duration, Instant}; + + use super::{Ended, spawn, wait}; + + fn sh(script: &str) -> Command { + let mut command = Command::new("sh"); + command.args(["-c", script]).stdin(Stdio::null()); + command + } + + #[test] + fn exit_status_is_kept() { + let mut child = spawn(&mut sh("exit 3")).unwrap(); + let ended = wait(&mut child, Duration::from_secs(10), Duration::from_secs(1)).unwrap(); + assert!( + matches!(ended, Ended::Exited(status) if status.code() == Some(3)), + "{ended:?}" + ); + } + + #[test] + fn timeout_terminates_the_test() { + let started = Instant::now(); + let mut child = spawn(&mut sh("sleep 30")).unwrap(); + let ended = wait( + &mut child, + Duration::from_millis(200), + Duration::from_secs(10), + ) + .unwrap(); + assert!(matches!(ended, Ended::TimedOut), "{ended:?}"); + // `sleep` dies on SIGTERM, well inside the grace period. + assert!(started.elapsed() < Duration::from_secs(5)); + } + + #[test] + fn a_test_ignoring_sigterm_is_killed_after_the_grace_period() { + let started = Instant::now(); + let mut child = spawn(&mut sh("trap '' TERM; sleep 30")).unwrap(); + let ended = wait( + &mut child, + Duration::from_millis(200), + Duration::from_millis(500), + ) + .unwrap(); + assert!(matches!(ended, Ended::TimedOut), "{ended:?}"); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_millis(700), "{elapsed:?}"); + assert!(elapsed < Duration::from_secs(5), "{elapsed:?}"); + } +} diff --git a/tests/testkit/testkit/src/program/supervisor.rs b/tests/testkit/testkit/src/program/supervisor.rs new file mode 100644 index 0000000000..6dced12ae1 --- /dev/null +++ b/tests/testkit/testkit/src/program/supervisor.rs @@ -0,0 +1,334 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `run`: run the selected tests one at a time, each in a new process. +//! +//! The program starts itself once per test as ` --exact --nocapture`, +//! the same interface nextest uses, in a new process group. A test that runs +//! past its timeout gets `SIGTERM`, a grace period, then `SIGKILL`. Output is +//! shown as it comes and kept for the `JUnit` report. +//! +//! A test stopped this way does not run its cleanup: testkit tests do not +//! handle `SIGTERM` yet, so sandboxes named `test--*` can be left +//! behind. + +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode, ExitStatus, Stdio}; +use std::time::{Duration, Instant}; + +use super::capture::Capture; +use super::process::{self, Ended}; +use super::{ERROR_EXIT, FAILED_EXIT, SUPERVISED_ENV, junit}; +use crate::registry::{Disposition, Selection, Test}; + +/// libtest's exit code when a test failed. +const LIBTEST_FAILED: i32 = 101; + +pub struct Options { + pub selection: Selection, + pub timeout: Duration, + pub grace: Duration, + pub junit: Option, +} + +/// One test's result. +pub struct TestRun<'a> { + pub test: &'a Test, + pub outcome: Outcome, + /// `None` for a test that did not run. + pub duration: Option, + pub stdout: Vec, + pub stderr: Vec, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Outcome { + Passed, + /// Ignored and not asked for. + Skipped, + Failed(String), + TimedOut(String), + /// The test could not be run. + Error(String), + /// Stopped because the run was interrupted. + Interrupted, +} + +impl Outcome { + fn label(&self) -> &'static str { + match self { + Self::Passed => "PASS", + Self::Skipped => "SKIP", + Self::Failed(_) => "FAIL", + Self::TimedOut(_) => "TIMEOUT", + Self::Error(_) | Self::Interrupted => "ERROR", + } + } + + fn message(&self) -> Option<&str> { + match self { + Self::Passed | Self::Skipped => None, + Self::Failed(message) | Self::TimedOut(message) | Self::Error(message) => Some(message), + Self::Interrupted => Some("interrupted"), + } + } +} + +pub fn run(program: &str, exe: &Path, tests: &[Test], options: &Options) -> ExitCode { + let selected = options.selection.apply(tests); + let to_run = selected + .iter() + .filter(|(_, disposition)| *disposition == Disposition::Run) + .count(); + if to_run == 0 { + eprintln!("{program}: no tests to run; `{program} list` shows the tests"); + return ExitCode::from(ERROR_EXIT); + } + if let Err(error) = process::install_stop_handlers() { + eprintln!("{program}: install signal handlers: {error}"); + return ExitCode::from(ERROR_EXIT); + } + let skipped = selected.len() - to_run; + eprintln!( + "{program}: running {to_run} test{} one at a time{}", + plural(to_run), + if skipped > 0 { + format!(", skipping {skipped} ignored") + } else { + String::new() + } + ); + + let started = Instant::now(); + let selected_count = selected.len(); + let mut runs = Vec::with_capacity(selected_count); + let mut position = 0; + for (test, disposition) in selected { + // A stop signal that arrives between tests still ends the run. + if process::stop_requested() { + break; + } + if disposition == Disposition::Skip { + runs.push(TestRun { + test, + outcome: Outcome::Skipped, + duration: None, + stdout: Vec::new(), + stderr: Vec::new(), + }); + continue; + } + position += 1; + let name = test.name(); + eprintln!("==> [{position}/{to_run}] {name}"); + let run = run_one(exe, test, options); + let elapsed = run.duration.unwrap_or_default(); + match run.outcome.message() { + None => eprintln!("<== {} {name} ({elapsed:.1?})", run.outcome.label()), + Some(message) => eprintln!( + "<== {} {name} ({elapsed:.1?}): {message}", + run.outcome.label() + ), + } + runs.push(run); + } + + let not_run = selected_count - runs.len(); + let mut status = summarize(program, &runs, not_run, started.elapsed()); + if let Some(path) = &options.junit { + match junit::write(path, &junit::report(program, &runs)) { + Ok(()) => eprintln!("{program}: wrote {}", path.display()), + Err(error) => { + eprintln!("{program}: write {}: {error}", path.display()); + status = ERROR_EXIT; + } + } + } + ExitCode::from(status) +} + +fn run_one<'a>(exe: &Path, test: &'a Test, options: &Options) -> TestRun<'a> { + let name = test.name(); + let mut command = Command::new(exe); + command + .args([name.as_str(), "--exact", "--nocapture"]) + .env(SUPERVISED_ENV, "1") + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + if test.ignored { + command.arg("--ignored"); + } + + let started = Instant::now(); + let mut child = match process::spawn(&mut command) { + Ok(child) => child, + Err(error) => { + return TestRun { + test, + outcome: Outcome::Error(format!("start {}: {error}", exe.display())), + duration: Some(started.elapsed()), + stdout: Vec::new(), + stderr: Vec::new(), + }; + } + }; + let stdout = child + .stdout + .take() + .map(|stream| Capture::start(stream, std::io::stdout())); + let stderr = child + .stderr + .take() + .map(|stream| Capture::start(stream, std::io::stderr())); + let ended = process::wait(&mut child, options.timeout, options.grace); + let duration = started.elapsed(); + let outcome = match ended { + Ok(ended) => outcome(ended, options.timeout), + Err(error) => Outcome::Error(format!("wait for the test process: {error}")), + }; + TestRun { + test, + outcome, + duration: Some(duration), + stdout: stdout.map(Capture::finish).unwrap_or_default(), + stderr: stderr.map(Capture::finish).unwrap_or_default(), + } +} + +fn outcome(ended: Ended, timeout: Duration) -> Outcome { + match ended { + Ended::Exited(status) => exited(status), + Ended::TimedOut => Outcome::TimedOut(format!( + "timed out after {}; stopped without cleanup", + seconds(timeout) + )), + Ended::Interrupted => Outcome::Interrupted, + } +} + +fn exited(status: ExitStatus) -> Outcome { + match status.code() { + Some(0) => Outcome::Passed, + Some(LIBTEST_FAILED) => Outcome::Failed("test failed".to_string()), + Some(code) if code == i32::from(ERROR_EXIT) => { + Outcome::Error("the test process could not run the test; see its output".to_string()) + } + Some(code) => Outcome::Failed(format!("test process exited with code {code}")), + None => Outcome::Failed(format!("test process ended by {status}")), + } +} + +/// Print the totals and the tests that did not pass; return the exit code: +/// 1 if a test failed, 2 if one could not run or the run was interrupted +/// before `not_run` tests. +fn summarize(program: &str, runs: &[TestRun<'_>], not_run: usize, elapsed: Duration) -> u8 { + let count = + |predicate: fn(&Outcome) -> bool| runs.iter().filter(|run| predicate(&run.outcome)).count(); + let passed = count(|outcome| matches!(outcome, Outcome::Passed)); + let failed = count(|outcome| matches!(outcome, Outcome::Failed(_) | Outcome::TimedOut(_))); + let errors = count(|outcome| matches!(outcome, Outcome::Error(_) | Outcome::Interrupted)); + let skipped = count(|outcome| matches!(outcome, Outcome::Skipped)); + let mut summary = format!("{program}: {passed} passed, {failed} failed"); + if errors > 0 { + let _ = write!(summary, ", {errors} could not run"); + } + if skipped > 0 { + let _ = write!(summary, ", {skipped} skipped"); + } + let _ = write!(summary, " in {elapsed:.1?}"); + eprintln!("{summary}"); + for run in runs { + if let Some(message) = run.outcome.message() { + eprintln!(" {} {}: {message}", run.outcome.label(), run.test.name()); + } + } + if not_run > 0 { + eprintln!( + "{program}: interrupted; {not_run} later test{} did not run", + plural(not_run) + ); + } + if errors > 0 || not_run > 0 { + ERROR_EXIT + } else if failed > 0 { + FAILED_EXIT + } else { + 0 + } +} + +fn seconds(duration: Duration) -> String { + format!("{}s", duration.as_secs_f64()) +} + +fn plural(count: usize) -> &'static str { + if count == 1 { "" } else { "s" } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::{Outcome, TestRun, summarize}; + use crate::registry::Test; + use crate::registry::tests::test; + + const TEST: Test = test("suite::m", "t", false); + + fn runs(outcomes: Vec) -> Vec> { + outcomes + .into_iter() + .map(|outcome| TestRun { + test: &TEST, + outcome, + duration: None, + stdout: Vec::new(), + stderr: Vec::new(), + }) + .collect() + } + + #[test] + fn exit_code_separates_failures_from_runs_that_could_not_complete() { + let code = |outcomes| summarize("p", &runs(outcomes), 0, Duration::ZERO); + assert_eq!(code(vec![Outcome::Passed, Outcome::Skipped]), 0); + assert_eq!(code(vec![Outcome::Passed, Outcome::Failed("f".into())]), 1); + assert_eq!(code(vec![Outcome::TimedOut("t".into())]), 1); + assert_eq!( + code(vec![ + Outcome::Failed("f".into()), + Outcome::Error("e".into()) + ]), + 2 + ); + assert_eq!(code(vec![Outcome::Interrupted]), 2); + // Interrupted between tests: every test that ran passed. + assert_eq!( + summarize("p", &runs(vec![Outcome::Passed]), 1, Duration::ZERO), + 2 + ); + } + + #[test] + fn exit_statuses_map_to_outcomes() { + use std::os::unix::process::ExitStatusExt; + + use super::exited; + + let status = |code: i32| std::process::ExitStatus::from_raw(code << 8); + assert_eq!(exited(status(0)), Outcome::Passed); + assert_eq!(exited(status(101)), Outcome::Failed("test failed".into())); + assert!(matches!(exited(status(2)), Outcome::Error(_))); + assert_eq!( + exited(status(7)), + Outcome::Failed("test process exited with code 7".into()) + ); + let killed = exited(std::process::ExitStatus::from_raw(libc::SIGSEGV)); + assert!( + matches!(&killed, Outcome::Failed(message) if message.contains("signal")), + "{killed:?}" + ); + } +} diff --git a/tests/testkit/testkit/src/ready.rs b/tests/testkit/testkit/src/ready.rs new file mode 100644 index 0000000000..db36f67659 --- /dev/null +++ b/tests/testkit/testkit/src/ready.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Waiting until the gateway is ready for tests. + +use std::time::Duration; + +use crate::cli::Cli; +use crate::gateway::Gateway; + +/// Wait until the gateway selected as for tests is healthy and the CLI +/// reports it connected, logging to stderr as `program`. Returns whether it +/// is ready; if not, the last error has been logged. +pub async fn check(program: &str, timeout: Duration) -> bool { + let result = async { + let gateway = Gateway::from_env()?; + eprintln!("{program}: {}", gateway.describe()); + let health = gateway.wait_ready(timeout).await?; + Cli::from_env(&gateway).check_connected().await?; + crate::Result::Ok(health) + } + .await; + match result { + Ok(health) => { + eprintln!( + "{program}: gateway {} is healthy and the CLI reaches it", + health.version + ); + true + } + Err(error) => { + eprintln!("{program}: {error}"); + false + } + } +} diff --git a/tests/testkit/testkit/src/registry.rs b/tests/testkit/testkit/src/registry.rs new file mode 100644 index 0000000000..aceaed673f --- /dev/null +++ b/tests/testkit/testkit/src/registry.rs @@ -0,0 +1,208 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! The tests linked into a program, registered by `#[testkit::test]`. + +use std::collections::BTreeSet; +use std::future::Future; +use std::pin::Pin; + +use crate::run::TestInfo; + +/// Every `#[testkit::test]` linked into the program. +#[linkme::distributed_slice] +pub static TESTS: [Test]; + +/// A registered test. +#[derive(Debug)] +pub struct Test { + pub info: TestInfo, + /// The directory under `tests/suites/` holding the test's crate, if any. + pub family: Option<&'static str>, + /// `module_path!()` where the test is defined. + pub module_path: &'static str, + pub ignored: bool, + /// The reason given in `#[ignore = "..."]`. + pub ignore_reason: Option<&'static str>, + /// Runs the test through [`crate::run`]; panics to fail it. + pub run: fn() -> Pin>>, +} + +impl Test { + /// The test's name, as libtest and nextest name it: the module path + /// without the crate, then the function name. + pub fn name(&self) -> String { + match self.module_path.split_once("::") { + Some((_crate, module)) => format!("{module}::{}", self.info.name), + None => self.info.name.to_string(), + } + } + + pub fn family(&self) -> &'static str { + self.family.unwrap_or("-") + } +} + +/// Names a test cannot have at the top level: the program would read them as +/// its own subcommands, so nextest could not run the test by name. +pub const RESERVED_NAMES: &[&str] = &["help", "list", "ready", "run"]; + +/// Check the tests can be told apart by name. +pub fn validate(tests: &[Test]) -> Result<(), String> { + let mut names = BTreeSet::new(); + for test in tests { + let name = test.name(); + if RESERVED_NAMES.contains(&name.as_str()) { + return Err(format!( + "test {name} has the name of a subcommand; rename it or move it into a module" + )); + } + if !names.insert(name.clone()) { + return Err(format!( + "two tests are named {name}; test names must be unique in a program" + )); + } + } + Ok(()) +} + +/// Which tests to run. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Selection { + /// Run tests whose name contains any of these; all tests if empty. + pub filters: Vec, + /// Run `#[ignore]`d tests too, instead of reporting them as skipped. + pub include_ignored: bool, +} + +/// What a selection does with one test. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Disposition { + Run, + /// Selected, but ignored and not asked for. + Skip, +} + +impl Selection { + /// The tests this selects, in name order, with what to do with each. + pub fn apply<'a>(&self, tests: &'a [Test]) -> Vec<(&'a Test, Disposition)> { + let mut selected: Vec<_> = tests + .iter() + .filter_map(|test| { + self.disposition(test) + .map(|disposition| (test, disposition)) + }) + .collect(); + selected.sort_by_cached_key(|(test, _)| test.name()); + selected + } + + fn disposition(&self, test: &Test) -> Option { + let name = test.name(); + if !self.filters.is_empty() + && !self + .filters + .iter() + .any(|filter| name.contains(filter.as_str())) + { + return None; + } + if test.ignored && !self.include_ignored { + Some(Disposition::Skip) + } else { + Some(Disposition::Run) + } + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::{Disposition, Selection, Test, validate}; + use crate::run::TestInfo; + + pub(crate) const fn test(module_path: &'static str, name: &'static str, ignored: bool) -> Test { + Test { + info: TestInfo { + name, + description: "A test.", + }, + family: Some("conformance"), + module_path, + ignored, + ignore_reason: None, + run: || Box::pin(async {}), + } + } + + const TESTS: &[Test] = &[ + test("sandbox::smoke", "create_exec_delete", false), + test("sandbox::lifecycle", "stop_start", false), + test("sandbox::lifecycle", "restart_later", true), + test("smoke", "top_level", false), + ]; + + fn names(selection: &Selection) -> Vec<(String, Disposition)> { + selection + .apply(TESTS) + .into_iter() + .map(|(test, disposition)| (test.name(), disposition)) + .collect() + } + + #[test] + fn names_follow_libtest() { + assert_eq!(TESTS[0].name(), "smoke::create_exec_delete"); + assert_eq!(TESTS[3].name(), "top_level"); + } + + #[test] + fn selection_follows_libtest() { + use Disposition::{Run, Skip}; + let all = Selection::default(); + assert_eq!( + names(&all), + [ + ("lifecycle::restart_later".into(), Skip), + ("lifecycle::stop_start".into(), Run), + ("smoke::create_exec_delete".into(), Run), + ("top_level".into(), Run), + ] + ); + + let filtered = Selection { + filters: vec!["stop".into(), "top".into()], + ..Selection::default() + }; + assert_eq!( + names(&filtered), + [ + ("lifecycle::stop_start".into(), Run), + ("top_level".into(), Run) + ] + ); + + let include = Selection { + filters: vec!["lifecycle".into()], + include_ignored: true, + }; + assert_eq!( + names(&include), + [ + ("lifecycle::restart_later".into(), Run), + ("lifecycle::stop_start".into(), Run) + ] + ); + } + + #[test] + fn validate_rejects_ambiguous_names() { + assert!(validate(TESTS).is_ok()); + let duplicate = [test("a::m", "t", false), test("b::m", "t", false)]; + let error = validate(&duplicate).unwrap_err(); + assert!(error.contains("two tests are named m::t"), "{error}"); + let reserved = [test("suite", "run", false)]; + let error = validate(&reserved).unwrap_err(); + assert!(error.contains("name of a subcommand"), "{error}"); + assert!(validate(&[test("suite::m", "run", false)]).is_ok()); + } +} diff --git a/tests/testkit/testkit/tests/attribute.rs b/tests/testkit/testkit/tests/attribute.rs new file mode 100644 index 0000000000..1f49adea40 --- /dev/null +++ b/tests/testkit/testkit/tests/attribute.rs @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! What `#[testkit::test]` expands to, checked by compiling tests with it in +//! an ordinary libtest target. The tests need a gateway, so they are ignored; +//! the check compares the registry with what libtest lists. + +use std::collections::BTreeSet; +use std::process::Command; + +/// Ignored with a reason. +#[testkit::test] +#[ignore = "needs a gateway"] +async fn ignored_with_reason(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +/// Lint attributes reach the body: `cx` is unused. +#[testkit::test] +#[ignore] +#[allow(unused_variables)] +async fn lint_allowed(cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +/// Compiled out, so neither a test nor registered. +#[testkit::test] +#[cfg(any())] +async fn compiled_out(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +mod nested { + /// Named with its module. + #[testkit::test] + #[ignore] + async fn in_a_module(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) + } +} + +/// The names libtest lists for this binary, with `extra` arguments. +fn libtest_list(extra: &[&str]) -> BTreeSet { + let output = Command::new(std::env::current_exe().expect("current exe")) + .args(["--list", "--format", "terse"]) + .args(extra) + .output() + .expect("list the tests"); + assert!(output.status.success(), "{output:?}"); + String::from_utf8(output.stdout) + .expect("utf-8") + .lines() + .filter_map(|line| line.strip_suffix(": test")) + .filter(|name| *name != "registry_matches_libtest") + .map(str::to_string) + .collect() +} + +#[test] +fn registry_matches_libtest() { + let registered: Vec<_> = testkit::__private::TESTS + .iter() + .map(|test| { + ( + test.name(), + test.module_path, + test.family, + test.ignored, + test.ignore_reason, + test.info.description, + ) + }) + .collect::>() + .into_iter() + .collect(); + assert_eq!( + registered, + [ + ( + "ignored_with_reason".to_string(), + "attribute", + None, + true, + Some("needs a gateway"), + "Ignored with a reason." + ), + ( + "lint_allowed".to_string(), + "attribute", + None, + true, + None, + "Lint attributes reach the body: `cx` is unused." + ), + ( + "nested::in_a_module".to_string(), + "attribute::nested", + None, + true, + None, + "Named with its module." + ), + ] + ); + + // libtest runs and ignores the same tests, under the same names. + let names = |ignored_only: bool| -> BTreeSet { + testkit::__private::TESTS + .iter() + .filter(|test| test.ignored || !ignored_only) + .map(testkit::Test::name) + .collect() + }; + assert_eq!(libtest_list(&[]), names(false)); + assert_eq!(libtest_list(&["--ignored"]), names(true)); +} diff --git a/tests/testkit/testkit/tests/program.rs b/tests/testkit/testkit/tests/program.rs new file mode 100644 index 0000000000..c414e8dd27 --- /dev/null +++ b/tests/testkit/testkit/tests/program.rs @@ -0,0 +1,527 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Checks of `testkit::main()`, as a test program. +//! +//! This binary is three programs, chosen by `TESTKIT_FIXTURE`: +//! +//! - unset: the checks below, run by `cargo test` and nextest through the +//! program's libtest interface; +//! - `fixtures`: tests that pass, fail, hang, ignore `SIGTERM`, or leave +//! processes behind, which the checks run through the program's +//! subcommands; +//! - `registered`: `testkit::main()` over the `#[testkit::test]`s below; +//! - `empty`: a program with no tests. + +#[cfg(unix)] +fn main() -> std::process::ExitCode { + match std::env::var("TESTKIT_FIXTURE").as_deref() { + Ok("fixtures") => testkit::program::main_with(&unix::FIXTURES), + Ok("registered") => testkit::main(), + Ok("empty") => testkit::program::main_with(&[]), + _ => testkit::program::main_with(&unix::CHECKS), + } +} + +#[cfg(not(unix))] +fn main() {} + +/// Needs a gateway. +#[testkit::test] +async fn registered_needs_a_gateway(cx: &mut testkit::Context) -> testkit::Result { + cx.cli(["status"]).await?.success()?; + Ok(()) +} + +/// Ignored until later. +#[testkit::test] +#[ignore = "https://github.com/NVIDIA/OpenShell/issues/1"] +async fn registered_ignored(_cx: &mut testkit::Context) -> testkit::Result { + Ok(()) +} + +#[cfg(unix)] +mod unix { + use std::future::Future; + use std::os::unix::process::CommandExt as _; + use std::path::{Path, PathBuf}; + use std::pin::Pin; + use std::process::{Child, Command, Output, Stdio}; + use std::time::{Duration, Instant}; + + use testkit::{Test, TestInfo}; + + const PID_FILE_ENV: &str = "FIXTURE_PID_FILE"; + + type Run = fn() -> Pin>>; + + const fn test(module_path: &'static str, name: &'static str, run: Run) -> Test { + Test { + info: TestInfo { + name, + description: "A fixture.", + }, + family: Some("fixtures"), + module_path, + ignored: false, + ignore_reason: None, + run, + } + } + + const fn fixture(name: &'static str, run: Run) -> Test { + test("program::fixture", name, run) + } + + const fn check(name: &'static str, run: Run) -> Test { + test("program::check", name, run) + } + + fn spawn_helper_and_record_its_pid() { + // A helper in the test's process group, like a CLI process. It is + // never waited on: the supervisor must kill it. + #[allow(clippy::zombie_processes)] + let helper = Command::new("sleep") + .arg("300") + .spawn() + .expect("spawn sleep"); + let pid_file = std::env::var(PID_FILE_ENV).expect("pid file"); + std::fs::write(pid_file, helper.id().to_string()).expect("write pid"); + } + + pub static FIXTURES: [Test; 7] = [ + fixture("passes", || { + Box::pin(async { + println!("passes: stdout"); + eprintln!("passes: stderr"); + }) + }), + fixture("fails", || Box::pin(async { panic!("fixture failure") })), + fixture("hangs", || { + Box::pin(async { + spawn_helper_and_record_its_pid(); + std::thread::sleep(Duration::from_secs(300)); + }) + }), + fixture("leaves_a_helper", || { + Box::pin(async { spawn_helper_and_record_its_pid() }) + }), + fixture("interrupts_after_exit", || { + Box::pin(async { + // Signal the supervisor once this test has exited, while it + // waits for the test's output to close. The helper leaves + // the test's process group so that it survives the test, and + // holds the output open until it has sent the signal. + let supervisor = std::os::unix::process::parent_id(); + #[allow(clippy::zombie_processes)] + let _signaller = Command::new("sh") + .args(["-c", &format!("sleep 1; kill -TERM {supervisor}")]) + .process_group(0) + .spawn() + .expect("spawn the signaller"); + }) + }), + fixture("ignores_sigterm", || { + Box::pin(async { + // SAFETY: plain syscall. + unsafe { + libc::signal(libc::SIGTERM, libc::SIG_IGN); + } + std::thread::sleep(Duration::from_secs(300)); + }) + }), + Test { + ignored: true, + ignore_reason: Some("not today"), + ..fixture("ignored", || Box::pin(async {})) + }, + ]; + + pub static CHECKS: [Test; 12] = [ + check("nextest_interface", || { + Box::pin(async { nextest_interface() }) + }), + check("list_shows_name_and_description", || { + Box::pin(async { list_shows_name_and_description() }) + }), + check("run_reports_every_outcome", || { + Box::pin(async { run_reports_every_outcome() }) + }), + check("run_exit_codes", || Box::pin(async { run_exit_codes() })), + check("a_program_without_tests_fails", || { + Box::pin(async { a_program_without_tests_fails() }) + }), + check("timeout_kills_the_process_group", || { + Box::pin(async { timeout_kills_the_process_group() }) + }), + check("sigterm_is_followed_by_sigkill", || { + Box::pin(async { sigterm_is_followed_by_sigkill() }) + }), + check("interrupt_stops_the_test_and_the_run", || { + Box::pin(async { interrupt_stops_the_test_and_the_run() }) + }), + check("interrupt_between_tests_ends_the_run", || { + Box::pin(async { interrupt_between_tests_ends_the_run() }) + }), + check("a_passing_test_leaves_nothing_behind", || { + Box::pin(async { a_passing_test_leaves_nothing_behind() }) + }), + check("registered_tests_run_through_testkit", || { + Box::pin(async { registered_tests_run_through_testkit() }) + }), + check("ready_fails_without_a_gateway", || { + Box::pin(async { ready_fails_without_a_gateway() }) + }), + ]; + + fn program(fixture: &str, args: &[&str]) -> Command { + let mut command = Command::new(std::env::current_exe().expect("current exe")); + command + .args(args) + .env("TESTKIT_FIXTURE", fixture) + .env_remove("TESTKIT_SUPERVISED") + .stdin(Stdio::null()); + command + } + + fn output(mut command: Command) -> Output { + let output = command.output().expect("run the program"); + eprintln!( + "{command:?}: {}\n--- stdout\n{}--- stderr\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + output + } + + fn stdout(output: &Output) -> &str { + std::str::from_utf8(&output.stdout).expect("utf-8") + } + + fn stderr(output: &Output) -> &str { + std::str::from_utf8(&output.stderr).expect("utf-8") + } + + fn junit(dir: &Path) -> (PathBuf, String) { + let path = dir.join("results").join("junit.xml"); + (path.clone(), path.to_str().expect("utf-8 path").to_string()) + } + + /// nextest lists with `--list --format terse [--ignored]` and runs each + /// test with ` --exact --nocapture`. + fn nextest_interface() { + let list = output(program("fixtures", &["--list", "--format", "terse"])); + assert!(list.status.success()); + assert_eq!( + stdout(&list), + "fixture::fails: test\nfixture::hangs: test\nfixture::ignored: test\n\ + fixture::ignores_sigterm: test\nfixture::interrupts_after_exit: test\n\ + fixture::leaves_a_helper: test\nfixture::passes: test\n" + ); + let ignored = output(program( + "fixtures", + &["--list", "--format", "terse", "--ignored"], + )); + assert_eq!(stdout(&ignored), "fixture::ignored: test\n"); + + let pass = output(program( + "fixtures", + &["fixture::passes", "--exact", "--nocapture"], + )); + assert!(pass.status.success()); + assert!(stdout(&pass).contains("passes: stdout")); + assert!(stdout(&pass).contains("1 passed")); + + let fail = output(program( + "fixtures", + &["fixture::fails", "--exact", "--nocapture"], + )); + assert_eq!(fail.status.code(), Some(101)); + assert!(stderr(&fail).contains("fixture failure")); + + // An ignored test runs only when asked for. + let skip = output(program("fixtures", &["fixture::ignored", "--exact"])); + assert!(stdout(&skip).contains("1 ignored")); + let run = output(program( + "fixtures", + &["fixture::ignored", "--exact", "--ignored"], + )); + assert!(stdout(&run).contains("1 passed")); + } + + fn list_shows_name_and_description() { + let list = output(program("registered", &["list"])); + assert!(list.status.success()); + let lines: Vec<_> = stdout(&list).lines().collect(); + assert_eq!(lines.len(), 2, "{lines:?}"); + assert_eq!( + lines[0], + "registered_ignored Ignored until later. \ + [ignored: https://github.com/NVIDIA/OpenShell/issues/1]" + ); + assert_eq!(lines[1], "registered_needs_a_gateway Needs a gateway."); + + let filtered = output(program("fixtures", &["list", "pass"])); + assert_eq!(stdout(&filtered), "fixture::passes A fixture.\n"); + } + + fn run_reports_every_outcome() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(program( + "fixtures", + &["run", "passes", "fails", "ignored", "--junit", &junit_arg], + )); + assert_eq!(run.status.code(), Some(1)); + // Test output is passed through. + assert!(stdout(&run).contains("passes: stdout")); + assert!(stderr(&run).contains("passes: stderr")); + assert!(stderr(&run).contains("1 passed, 1 failed, 1 skipped")); + assert!(stderr(&run).contains("FAIL fixture::fails: test failed")); + + let xml = std::fs::read_to_string(path).expect("junit report"); + for expected in [ + r#""#, + r#""#, + r#""#, + "", + "passes: stdout", + "fixture failure", + ] { + assert!(xml.contains(expected), "missing {expected} in\n{xml}"); + } + } + + fn run_exit_codes() { + let pass = output(program("fixtures", &["run", "passes"])); + assert_eq!(pass.status.code(), Some(0)); + let ignored = output(program( + "fixtures", + &["run", "ignored", "--include-ignored"], + )); + assert_eq!(ignored.status.code(), Some(0)); + assert!(stderr(&ignored).contains("1 passed, 0 failed")); + // Selecting nothing to run is not a pass. + for args in [&["run", "nothing-matches"][..], &["run", "ignored"]] { + let none = output(program("fixtures", args)); + assert_eq!(none.status.code(), Some(2), "{args:?}"); + assert!(stderr(&none).contains("no tests to run")); + } + let usage = output(program("fixtures", &["run", "--timeout", "0"])); + assert_eq!(usage.status.code(), Some(2)); + } + + /// A program whose suites were not linked fails rather than passing with + /// no tests, under every interface. + fn a_program_without_tests_fails() { + for args in [ + &["list"][..], + &["run"], + &["--list", "--format", "terse"], + &[], + ] { + let empty = output(program("empty", args)); + assert_eq!(empty.status.code(), Some(2), "{args:?}"); + assert!( + stderr(&empty).contains("no tests are linked into this program"), + "{args:?}" + ); + } + } + + fn is_alive(pid: libc::pid_t) -> bool { + // SAFETY: signal 0 only checks the process exists. + unsafe { libc::kill(pid, 0) == 0 } + } + + /// Wait for a killed helper to go: once its parent is gone, init reaps it. + fn assert_gone(pid: libc::pid_t) { + let deadline = Instant::now() + Duration::from_secs(10); + while is_alive(pid) { + assert!(Instant::now() < deadline, "helper {pid} outlived its test"); + std::thread::sleep(Duration::from_millis(50)); + } + } + + fn read_pid(path: &Path) -> libc::pid_t { + let deadline = Instant::now() + Duration::from_secs(30); + loop { + if let Ok(pid) = std::fs::read_to_string(path) + && let Ok(pid) = pid.trim().parse() + { + return pid; + } + assert!(Instant::now() < deadline, "the hanging test never started"); + std::thread::sleep(Duration::from_millis(20)); + } + } + + fn timeout_kills_the_process_group() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let (path, junit_arg) = junit(dir.path()); + let mut command = program( + "fixtures", + &[ + "run", + "hangs", + "--timeout", + "1", + "--grace", + "5", + "--junit", + &junit_arg, + ], + ); + command.env(PID_FILE_ENV, &pid_file); + let started = Instant::now(); + let run = output(command); + assert_eq!(run.status.code(), Some(1)); + // `SIGTERM` stops the test well inside the grace period. + assert!( + started.elapsed() < Duration::from_secs(5), + "{:?}", + started.elapsed() + ); + assert!(stderr(&run).contains("TIMEOUT fixture::hangs: timed out after 1s")); + assert_gone(read_pid(&pid_file)); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(xml.contains(r#"type="timeout""#), "{xml}"); + } + + fn sigterm_is_followed_by_sigkill() { + let started = Instant::now(); + let run = output(program( + "fixtures", + &[ + "run", + "ignores_sigterm", + "--timeout", + "0.2", + "--grace", + "0.5", + ], + )); + assert_eq!(run.status.code(), Some(1)); + let elapsed = started.elapsed(); + assert!(elapsed >= Duration::from_millis(700), "{elapsed:?}"); + assert!(elapsed < Duration::from_secs(10), "{elapsed:?}"); + assert!(stderr(&run).contains("TIMEOUT fixture::ignores_sigterm")); + } + + fn interrupt_stops_the_test_and_the_run() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let (path, junit_arg) = junit(dir.path()); + let mut command = program( + "fixtures", + &[ + "run", "hangs", "passes", "--grace", "5", "--junit", &junit_arg, + ], + ); + command + .env(PID_FILE_ENV, &pid_file) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + let child: Child = command.spawn().expect("start the run"); + let helper = read_pid(&pid_file); + // SAFETY: plain syscall to our own child, not yet reaped. + unsafe { + libc::kill( + libc::pid_t::try_from(child.id()).expect("pid"), + libc::SIGTERM, + ); + } + let run = child.wait_with_output().expect("wait for the run"); + eprintln!("{}", String::from_utf8_lossy(&run.stderr)); + assert_eq!(run.status.code(), Some(2)); + assert!(stderr(&run).contains("ERROR fixture::hangs: interrupted")); + assert_gone(helper); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(xml.contains(r#"type="interrupted""#), "{xml}"); + assert!(!xml.contains("fixture::passes"), "{xml}"); + } + + fn interrupt_between_tests_ends_the_run() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(program( + "fixtures", + &[ + "run", + "interrupts_after_exit", + "passes", + "--junit", + &junit_arg, + ], + )); + assert_eq!(run.status.code(), Some(2)); + assert!(stderr(&run).contains("<== PASS fixture::interrupts_after_exit")); + assert!(stderr(&run).contains("interrupted; 1 later test did not run")); + assert!(!stdout(&run).contains("passes: stdout")); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!(!xml.contains("fixture::passes"), "{xml}"); + } + + fn a_passing_test_leaves_nothing_behind() { + let dir = tempfile::tempdir().expect("temp dir"); + let pid_file = dir.path().join("pid"); + let mut command = program("fixtures", &["run", "leaves_a_helper"]); + command.env(PID_FILE_ENV, &pid_file); + let run = output(command); + assert_eq!(run.status.code(), Some(0)); + assert_gone(read_pid(&pid_file)); + } + + /// Point the gateway lookup at a closed port, away from any registered + /// gateways. + fn without_gateway(mut command: Command, config: &Path) -> Command { + let closed = std::net::TcpListener::bind("127.0.0.1:0").expect("bind"); + let endpoint = format!("http://{}", closed.local_addr().expect("address")); + drop(closed); + command + .env("XDG_CONFIG_HOME", config) + .env("OPENSHELL_GATEWAY_ENDPOINT", endpoint) + .env_remove("OPENSHELL_GATEWAY") + .env("OPENSHELL_BIN", config.join("no-openshell")); + command + } + + fn registered_tests_run_through_testkit() { + let dir = tempfile::tempdir().expect("temp dir"); + let (path, junit_arg) = junit(dir.path()); + let run = output(without_gateway( + program("registered", &["run", "--junit", &junit_arg]), + dir.path(), + )); + assert_eq!(run.status.code(), Some(1)); + // `testkit::run` logs the description and the gateway. + assert!(stderr(&run).contains("[registered_needs_a_gateway] Needs a gateway.")); + assert!(stderr(&run).contains("1 failed, 1 skipped")); + let xml = std::fs::read_to_string(path).expect("junit report"); + assert!( + xml.contains(r#""#), + "{xml}" + ); + } + + fn ready_fails_without_a_gateway() { + let dir = tempfile::tempdir().expect("temp dir"); + let started = Instant::now(); + let ready = output(without_gateway( + program("registered", &["ready", "--timeout", "1"]), + dir.path(), + )); + assert_eq!(ready.status.code(), Some(2)); + assert!(stderr(&ready).contains("not ready after 1s")); + assert!(started.elapsed() < Duration::from_secs(10)); + } +} From 71da34fc14a09d9350c8ba306424fe158e443cae Mon Sep 17 00:00:00 2001 From: grs Date: Thu, 5 Nov 2026 15:08:38 +0000 Subject: [PATCH 11/31] fix(server): validate provider attachments against latest policy (#5310) Signed-off-by: Gordon Sim --- crates/openshell-server/src/grpc/policy.rs | 89 +++++++++++++++++++++ crates/openshell-server/src/grpc/sandbox.rs | 4 +- docs/how-it-works/providers/overview.mdx | 6 ++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index be95843f6b..f5c7c94480 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -13114,6 +13114,95 @@ mod tests { assert!(error.message().contains("already defines endpoints")); } + #[tokio::test] + async fn provider_attachment_credential_inspection_uses_latest_policy() { + for (creation_has_rule, updated_has_rule) in [ + (true, Some(false)), + (false, Some(true)), + (true, None), + (false, None), + ] { + let state = test_server_state().await; + state + .store + .put_message(&test_provider("work-github", "github")) + .await + .unwrap(); + let policy = |has_rule| { + if has_rule { + test_policy_with_rule("uninspected", "api.github.com") + } else { + ProtoSandboxPolicy::default() + } + }; + let creation_policy = policy(creation_has_rule); + let sandbox = test_sandbox( + "sb-attachment-inspection", + "attach-inspection", + creation_policy.clone(), + Vec::new(), + ); + state.store.put_message(&sandbox).await.unwrap(); + + if let Some(has_rule) = updated_has_rule { + handle_update_config( + &state, + with_user(Request::new(UpdateConfigRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + policy: Some(policy(has_rule)), + ..Default::default() + })), + ) + .await + .expect("policy update without an attached provider must succeed"); + } + + let result = super::super::sandbox::handle_attach_sandbox_provider( + &state, + authed_request(openshell_core::proto::AttachSandboxProviderRequest { + sandbox: "attach-inspection".to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + provider: "work-github".to_string(), + ..Default::default() + }), + ) + .await; + let should_reject = updated_has_rule.unwrap_or(creation_has_rule); + if should_reject { + let error = result.expect_err("current uninspected rule must prevent attachment"); + assert_eq!(error.code(), Code::FailedPrecondition); + assert!(error.message().contains("L4-only")); + assert!(error.message().contains("uninspected")); + } else { + assert!( + result + .expect("no current uninspected rule") + .into_inner() + .attached + ); + } + + let stored = state + .store + .get_message::("sb-attachment-inspection") + .await + .unwrap() + .unwrap(); + let spec = stored.spec.unwrap(); + assert_eq!(spec.policy, Some(creation_policy)); + assert_eq!( + spec.providers, + if should_reject { + Vec::::new() + } else { + vec!["work-github".to_string()] + } + ); + state.store.close_for_test().await; + } + } + #[tokio::test] async fn update_config_gates_uninspected_endpointless_credential_binding() { use openshell_core::proto::{ProviderProfile, ProviderProfileCategory}; diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index c998372edb..86ff12b170 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1566,11 +1566,13 @@ pub(super) async fn handle_attach_sandbox_provider( &candidate_spec.providers, ) .await?; + let current_policy = + super::policy::current_base_policy_for_sandbox(state.store.as_ref(), &sandbox).await?; super::policy::validate_candidate_sandbox_credential_policy( state, &workspace, &candidate_spec.providers, - candidate_spec.policy.as_ref(), + Some(¤t_policy), ) .await?; diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index 73f209738e..bcfd476050 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -409,6 +409,12 @@ and `openshell sandbox provider detach`. See +Provider attachment checks credential inspection against the latest saved sandbox +policy, falling back to the creation policy only when no revision exists. If a +current rule permits uninspected traffic to a credentialed endpoint, configure +L7 inspection or remove the rule before attaching the provider. Use +`allow_uninspected_credentials: true` only for an intentional exception. + ### Auto-Discovery Shortcut Name a provider with `--provider`. If no provider by that name exists and the From ef83ee264007c5373bd9fcaa567fd0f26762e69b Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Thu, 5 Nov 2026 21:45:41 +0000 Subject: [PATCH 12/31] test(e2e): split credential_gating into independent parallel tests (#5264) The single credential gating test ran six sandbox lifecycles and two policy updates in sequence under fixed provider and profile names. On the Kubernetes smoke lanes it took 180-290 s and regularly hit the 300 s nextest cap. Split it into one test per gated-endpoint scenario. Each test installs a provider and profile under its own unique name, deletes them on drop (also on failure), and prints timed progress lines to stderr so a slow step is visible when a test times out. Closes #5263 Signed-off-by: Matthew Grossman --- e2e/rust/tests/credential_gating.rs | 653 ++++++++++++++++------------ 1 file changed, 385 insertions(+), 268 deletions(-) diff --git a/e2e/rust/tests/credential_gating.rs b/e2e/rust/tests/credential_gating.rs index bf0d6ae9b5..110e02cb1e 100644 --- a/e2e/rust/tests/credential_gating.rs +++ b/e2e/rust/tests/credential_gating.rs @@ -4,15 +4,20 @@ #![cfg(feature = "e2e")] //! E2E coverage for credentialed endpoint admission and REST body backstops. +//! +//! Each test installs its own provider and profile under a unique name, so the +//! tests can run in parallel. Each test prints its steps to stderr with the +//! elapsed time, so a timeout shows which step was slow. +use std::fmt::Display; use std::io::Write; use std::process::Stdio; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use std::time::Duration; +use std::time::{Duration, Instant}; use base64::Engine as _; -use openshell_e2e::harness::binary::openshell_cmd; +use openshell_e2e::harness::binary::{openshell_bin, openshell_cmd}; use openshell_e2e::harness::sandbox::SandboxGuard; use sha1::{Digest, Sha1}; use tempfile::NamedTempFile; @@ -20,8 +25,6 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; use tokio::task::JoinHandle; -const PROFILE_ID: &str = "e2e-credential-gating"; -const PROVIDER_NAME: &str = "e2e-credential-gating"; const TEST_HOST: &str = "host.openshell.internal"; const TOKEN_ENV: &str = "E2E_GATING_TOKEN"; const TEST_SECRET: &str = "e2e-gating-secret-value"; @@ -53,21 +56,68 @@ async fn run_cli(args: &[&str]) -> (bool, String) { ) } +/// Prints a test's steps to stderr with the time since the test started. +#[derive(Clone, Copy)] +struct Progress { + scenario: &'static str, + started: Instant, +} + +impl Progress { + fn start(scenario: &'static str) -> Self { + let progress = Self { + scenario, + started: Instant::now(), + }; + progress.step("start"); + progress + } + + fn step(self, message: impl Display) { + eprintln!( + "credential_gating[{}] +{:.1}s {message}", + self.scenario, + self.started.elapsed().as_secs_f64() + ); + } +} + +fn utf8_path(file: &NamedTempFile) -> Result<&str, String> { + file.path() + .to_str() + .ok_or_else(|| format!("{} is not UTF-8", file.path().display())) +} + /// Retry a delete until it takes effect. /// /// The gateway refuses to delete a provider or a profile that a sandbox still /// references, and sandbox teardown drains asynchronously, so a single attempt -/// can be rejected right after a sandbox was removed. -async fn delete_until_gone(args: &[&str]) -> Result<(), String> { +/// can be rejected right after a sandbox was removed. This runs from `Drop`, +/// so it blocks instead of awaiting. +fn delete_until_gone(args: &[&str]) -> Result<(), String> { const ATTEMPTS: u32 = 40; + let binary = openshell_bin(); let mut last_output = String::new(); for _ in 0..ATTEMPTS { - let (deleted, output) = run_cli(args).await; - if deleted || output.to_lowercase().contains("not found") { - return Ok(()); + match std::process::Command::new(&binary) + .args(args) + .stdin(Stdio::null()) + .output() + { + Ok(output) => { + let combined = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + if output.status.success() || combined.to_lowercase().contains("not found") { + return Ok(()); + } + last_output = combined; + } + Err(error) => last_output = format!("spawn openshell CLI: {error}"), } - last_output = output; - tokio::time::sleep(Duration::from_millis(500)).await; + std::thread::sleep(Duration::from_millis(500)); } Err(format!( "'{}' still failing after {ATTEMPTS} attempts:\n{last_output}", @@ -75,28 +125,97 @@ async fn delete_until_gone(args: &[&str]) -> Result<(), String> { )) } -/// Strict teardown for the install paths: every test resource must be gone -/// before it is recreated, otherwise creation fails with "already exists". -async fn ensure_provider_resources_absent() -> Result<(), String> { - delete_until_gone(&["provider", "delete", PROVIDER_NAME]).await?; - delete_until_gone(&["profile", "delete", PROFILE_ID]).await +/// A provider and the profile it uses, both under one name unique to the +/// test. Dropping it deletes both, also when the test fails. +struct GatingProvider { + name: String, + progress: Progress, } -/// Best-effort teardown. Never fails the test: it also runs on the failure -/// path, where the original assertion is the interesting one. -async fn cleanup_provider_resources() { - if let Err(error) = ensure_provider_resources_absent().await { - eprintln!("provider cleanup did not settle: {error}"); +impl GatingProvider { + async fn install_with_endpoints( + progress: Progress, + rest_port: u16, + websocket_port: u16, + ) -> Result { + let name = unique_name(progress.scenario); + let profile = write_provider_profile(&name, rest_port, websocket_port)?; + Self::install(progress, name, &profile).await + } + + async fn install_endpointless(progress: Progress) -> Result { + let name = unique_name(progress.scenario); + let profile = write_endpointless_provider_profile(&name)?; + Self::install(progress, name, &profile).await } + + async fn install( + progress: Progress, + name: String, + profile: &NamedTempFile, + ) -> Result { + progress.step(format_args!("importing profile {name}")); + let (imported, output) = + run_cli(&["profile", "import", "--file", utf8_path(profile)?]).await; + if !imported { + return Err(format!("profile import failed:\n{output}")); + } + let provider = Self { name, progress }; + progress.step(format_args!("creating provider {}", provider.name)); + let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); + let (created, output) = run_cli(&[ + "provider", + "create", + "--name", + &provider.name, + "--type", + &provider.name, + "--credential", + &credential, + ]) + .await; + if !created { + return Err(format!("provider create failed:\n{output}")); + } + Ok(provider) + } +} + +impl Drop for GatingProvider { + fn drop(&mut self) { + self.progress + .step(format_args!("deleting provider and profile {}", self.name)); + for kind in ["provider", "profile"] { + if let Err(error) = delete_until_gone(&[kind, "delete", &self.name]) { + self.progress + .step(format_args!("{kind} cleanup did not settle: {error}")); + } + } + self.progress.step("cleanup finished"); + } +} + +/// A provider and profile name that does not collide with parallel tests or +/// earlier runs against the same gateway. +fn unique_name(scenario: &str) -> String { + format!( + "e2e-cg-{scenario}-{}-{:08x}", + std::process::id(), + rand::random::() + ) } -fn write_provider_profile(rest_port: u16, websocket_port: u16) -> Result { +fn write_provider_profile( + profile_id: &str, + rest_port: u16, + websocket_port: u16, +) -> Result { let mut file = tempfile::Builder::new() .suffix(".yaml") .tempfile() .map_err(|error| format!("create profile: {error}"))?; let profile = format!( - r"id: {PROFILE_ID} + r"id: {profile_id} display_name: E2E Credential Gating category: other credentials: @@ -127,42 +246,13 @@ binaries: Ok(file) } -async fn install_provider(rest_port: u16, websocket_port: u16) -> Result<(), String> { - ensure_provider_resources_absent().await?; - let profile = write_provider_profile(rest_port, websocket_port)?; - let profile_path = profile - .path() - .to_str() - .ok_or_else(|| "profile path is not UTF-8".to_string())?; - let (imported, output) = run_cli(&["profile", "import", "--file", profile_path]).await; - if !imported { - return Err(format!("profile import failed:\n{output}")); - } - let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); - let (created, output) = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER_NAME, - "--type", - PROFILE_ID, - "--credential", - &credential, - ]) - .await; - if !created { - return Err(format!("provider create failed:\n{output}")); - } - Ok(()) -} - -fn write_endpointless_provider_profile() -> Result { +fn write_endpointless_provider_profile(profile_id: &str) -> Result { let mut file = tempfile::Builder::new() .suffix(".yaml") .tempfile() .map_err(|error| format!("create endpointless profile: {error}"))?; let profile = format!( - r"id: {PROFILE_ID} + r"id: {profile_id} display_name: E2E Endpointless Credential Gating category: other credentials: @@ -184,35 +274,6 @@ binaries: Ok(file) } -async fn install_endpointless_provider() -> Result<(), String> { - ensure_provider_resources_absent().await?; - let profile = write_endpointless_provider_profile()?; - let profile_path = profile - .path() - .to_str() - .ok_or_else(|| "endpointless profile path is not UTF-8".to_string())?; - let (imported, output) = run_cli(&["profile", "import", "--file", profile_path]).await; - if !imported { - return Err(format!("endpointless profile import failed:\n{output}")); - } - let credential = format!("{TOKEN_ENV}={TEST_SECRET}"); - let (created, output) = run_cli(&[ - "provider", - "create", - "--name", - PROVIDER_NAME, - "--type", - PROFILE_ID, - "--credential", - &credential, - ]) - .await; - if !created { - return Err(format!("endpointless provider create failed:\n{output}")); - } - Ok(()) -} - #[derive(Clone, Copy)] enum EndpointMode { L4, @@ -232,6 +293,7 @@ fn write_policy( port: u16, mode: EndpointMode, credential_source: CredentialSource, + provider: &str, ) -> Result { let mut file = NamedTempFile::new().map_err(|error| format!("create policy: {error}"))?; let endpoint_options = match mode { @@ -250,11 +312,11 @@ fn write_policy( let credential_binding = match credential_source { CredentialSource::ProviderProfile => String::new(), CredentialSource::PolicyBinding => { - format!(" credential_binding:\n provider: {PROVIDER_NAME}\n") + format!(" credential_binding:\n provider: {provider}\n") } }; let policy = format!( - r#"version: 1 + r"version: 1 filesystem_policy: include_workdir: true read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] @@ -274,7 +336,7 @@ network_policies: - path: /usr/bin/python* - path: /usr/local/bin/python* - path: /sandbox/.uv/python/*/bin/python* -"#, +", ); file.write_all(policy.as_bytes()) .map_err(|error| format!("write policy: {error}"))?; @@ -631,7 +693,15 @@ with socket.create_connection((host, port), timeout=10) as sock: ) } -async fn sandbox_create_failure(policy: &NamedTempFile) -> String { +async fn sandbox_create_failure( + progress: Progress, + label: &str, + provider: &str, + policy: &NamedTempFile, +) -> String { + progress.step(format_args!( + "{label}: creating sandbox, expecting rejection" + )); let policy_path = policy.path().to_str().expect("policy path is UTF-8"); let (success, output) = run_cli(&[ "sandbox", @@ -639,54 +709,71 @@ async fn sandbox_create_failure(policy: &NamedTempFile) -> String { "--policy", policy_path, "--provider", - PROVIDER_NAME, + provider, "--", "echo", "must-not-run", ]) .await; assert!(!success, "sandbox create unexpectedly succeeded:\n{output}"); + progress.step(format_args!("{label}: rejected")); output } +/// Create a sandbox, run one command in it, and delete it. +/// +/// Returns the create output followed by the command output. +async fn run_in_sandbox( + progress: Progress, + label: &str, + create_args: &[&str], + command: &[&str], +) -> Result { + progress.step(format_args!("{label}: creating sandbox")); + let mut sandbox = SandboxGuard::create(create_args).await?; + progress.step(format_args!( + "{label}: sandbox {} ready, running command", + sandbox.name + )); + let output = sandbox.exec(command).await; + progress.step(format_args!("{label}: deleting sandbox {}", sandbox.name)); + sandbox.cleanup().await; + progress.step(format_args!("{label}: sandbox deleted")); + Ok(format!("{}{}", sandbox.create_output, output?)) +} + async fn assert_gateway_admission( + progress: Progress, + provider: &str, port: u16, credential_source: CredentialSource, ) -> Result<(), String> { - let l4 = write_policy(port, EndpointMode::L4, credential_source)?; - let l4_error = sandbox_create_failure(&l4).await; + let l4 = write_policy(port, EndpointMode::L4, credential_source, provider)?; + let l4_error = sandbox_create_failure(progress, "L4-only policy", provider, &l4).await; assert!(l4_error.contains("credentialed endpoint"), "{l4_error}"); assert!(l4_error.contains("L4-only"), "{l4_error}"); - let tls_skip = write_policy(port, EndpointMode::TlsSkip, credential_source)?; - let tls_error = sandbox_create_failure(&tls_skip).await; + let tls_skip = write_policy(port, EndpointMode::TlsSkip, credential_source, provider)?; + let tls_error = sandbox_create_failure(progress, "tls: skip policy", provider, &tls_skip).await; assert!(tls_error.contains("credentialed endpoint"), "{tls_error}"); assert!( tls_error.contains("tls:") && tls_error.contains("skip"), "{tls_error}" ); - let opt_in = write_policy(port, EndpointMode::L4OptIn, credential_source)?; - let opt_in_path = opt_in - .path() - .to_str() - .ok_or_else(|| "opt-in policy path is not UTF-8".to_string())?; + let opt_in = write_policy(port, EndpointMode::L4OptIn, credential_source, provider)?; let accepted_marker = match credential_source { CredentialSource::ProviderProfile => "OPT_IN_ACCEPTED", CredentialSource::PolicyBinding => "ENDPOINTLESS_OPT_IN_ACCEPTED", }; - let mut sandbox = SandboxGuard::create(&[ - "--policy", - opt_in_path, - "--provider", - PROVIDER_NAME, - "--", - "echo", - accepted_marker, - ]) + let output = run_in_sandbox( + progress, + "opt-in policy", + &["--policy", utf8_path(&opt_in)?, "--provider", provider], + &["echo", accepted_marker], + ) .await?; - assert!(sandbox.create_output.contains(accepted_marker)); - sandbox.cleanup().await; + assert!(output.contains(accepted_marker), "{output}"); Ok(()) } @@ -712,168 +799,120 @@ async fn wait_for_provider_key_presence( )) } -async fn assert_endpointless_provider_env_live_update(port: u16) -> Result<(), String> { +async fn set_policy(progress: Progress, sandbox: &str, policy_path: &str) -> Result<(), String> { + let (success, output) = run_cli(&[ + "policy", + "set", + sandbox, + "--policy", + policy_path, + "--wait", + "--timeout", + "120", + ]) + .await; + if !success { + return Err(format!("policy update failed:\n{output}")); + } + progress.step("policy applied"); + Ok(()) +} + +async fn assert_endpointless_provider_env_live_update( + progress: Progress, + provider: &str, + port: u16, +) -> Result<(), String> { let unbound = write_policy( port, EndpointMode::RestBody { rewrite: false }, CredentialSource::ProviderProfile, + provider, )?; let bound = write_policy( port, EndpointMode::RestBody { rewrite: false }, CredentialSource::PolicyBinding, + provider, )?; - let unbound_path = unbound - .path() - .to_str() - .ok_or_else(|| "unbound policy path is not UTF-8".to_string())?; - let bound_path = bound - .path() - .to_str() - .ok_or_else(|| "bound policy path is not UTF-8".to_string())?; + let unbound_path = utf8_path(&unbound)?; + let bound_path = utf8_path(&bound)?; + progress.step("creating long-running sandbox with the unbound policy"); let mut sandbox = SandboxGuard::create_keep_with_args( - &[ - "--policy", - unbound_path, - "--provider", - PROVIDER_NAME, - "--no-tty", - ], + &["--policy", unbound_path, "--provider", provider, "--no-tty"], &["sh", "-c", "echo PROVIDER_UPDATE_READY && sleep infinity"], "PROVIDER_UPDATE_READY", ) .await?; let result = async { + progress.step(format_args!( + "sandbox {} ready, waiting for the token to be absent", + sandbox.name + )); wait_for_provider_key_presence(&sandbox, false).await?; - let (success, output) = run_cli(&[ - "policy", - "set", - &sandbox.name, - "--policy", - bound_path, - "--wait", - "--timeout", - "120", - ]) - .await; - if !success { - return Err(format!("binding policy update failed:\n{output}")); - } + progress.step("binding the provider with policy set --wait"); + set_policy(progress, &sandbox.name, bound_path) + .await + .map_err(|error| format!("binding {error}"))?; + progress.step("waiting for the token placeholder"); wait_for_provider_key_presence(&sandbox, true).await?; - let (success, output) = run_cli(&[ - "policy", - "set", - &sandbox.name, - "--policy", - unbound_path, - "--wait", - "--timeout", - "120", - ]) - .await; - if !success { - return Err(format!("unbinding policy update failed:\n{output}")); - } + progress.step("unbinding the provider with policy set --wait"); + set_policy(progress, &sandbox.name, unbound_path) + .await + .map_err(|error| format!("unbinding {error}"))?; + progress.step("waiting for the token to be absent"); wait_for_provider_key_presence(&sandbox, false).await } .await; + progress.step(format_args!("deleting sandbox {}", sandbox.name)); sandbox.cleanup().await; + progress.step("sandbox deleted"); result } async fn run_body_sandbox( + progress: Progress, + provider: &str, port: u16, mode: EndpointMode, credential_source: CredentialSource, ) -> Result { - let policy = write_policy(port, mode, credential_source)?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "body policy path is not UTF-8".to_string())?; - let script = body_client_script(port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) - .await?; - let output = sandbox.create_output.clone(); - sandbox.cleanup().await; - Ok(output) -} - -async fn run_profile_body_sandbox(port: u16) -> Result { - let policy = write_policy( - port, - EndpointMode::RestBody { rewrite: false }, - CredentialSource::ProviderProfile, - )?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "body policy path is not UTF-8".to_string())?; + let policy = write_policy(port, mode, credential_source, provider)?; let script = body_client_script(port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) - .await?; - let output = sandbox.create_output.clone(); - sandbox.cleanup().await; - Ok(output) -} - -async fn assert_rest_body_preserves_placeholder(server: &HttpProbeServer) -> Result<(), String> { - let output = run_profile_body_sandbox(server.port).await?; - assert!(output.contains("BODY_TEXT")); - let observations = server.wait_for_observations(1).await; - assert_eq!(observations.len(), 1, "observations: {observations:?}"); - assert!(observations[0].saw_placeholder); - assert!(!observations[0].saw_secret); - Ok(()) + run_in_sandbox( + progress, + "REST body client", + &["--policy", utf8_path(&policy)?, "--provider", provider], + &["python3", "-c", &script], + ) + .await } -async fn assert_websocket_binary_denied(server: &BinaryWebSocketProbeServer) -> Result<(), String> { +async fn assert_websocket_binary_denied( + progress: Progress, + provider: &str, + server: &BinaryWebSocketProbeServer, +) -> Result<(), String> { let policy = write_policy( server.port, EndpointMode::WebSocket, CredentialSource::ProviderProfile, + provider, )?; - let policy_path = policy - .path() - .to_str() - .ok_or_else(|| "WebSocket policy path is not UTF-8".to_string())?; let script = binary_websocket_client_script(server.port); - let mut sandbox = SandboxGuard::create(&[ - "--policy", - policy_path, - "--provider", - PROVIDER_NAME, - "--", - "python3", - "-c", - &script, - ]) + let output = run_in_sandbox( + progress, + "WebSocket client", + &["--policy", utf8_path(&policy)?, "--provider", provider], + &["python3", "-c", &script], + ) .await?; - assert!(sandbox.create_output.contains("BINARY_DENIED")); - sandbox.cleanup().await; + assert!(output.contains("BINARY_DENIED"), "{output}"); assert!( server.wait_for_handshake().await, "upstream should receive the WebSocket handshake" @@ -885,56 +924,134 @@ async fn assert_websocket_binary_denied(server: &BinaryWebSocketProbeServer) -> Ok(()) } -#[tokio::test] -async fn credentialed_endpoint_gates_work_end_to_end() { +/// Start both probe servers and install a provider whose profile declares +/// their endpoints. +async fn profile_endpoint_fixture( + progress: Progress, +) -> (HttpProbeServer, BinaryWebSocketProbeServer, GatingProvider) { let server = HttpProbeServer::start().await.expect("start HTTP probe"); let websocket_server = BinaryWebSocketProbeServer::start() .await .expect("start WebSocket probe"); - install_provider(server.port, websocket_server.port) + let provider = + GatingProvider::install_with_endpoints(progress, server.port, websocket_server.port) + .await + .expect("install credentialed provider"); + (server, websocket_server, provider) +} + +/// Start the HTTP probe server and install a provider whose profile declares +/// no endpoints, so policies must bind it explicitly. +async fn endpointless_fixture(progress: Progress) -> (HttpProbeServer, GatingProvider) { + let server = HttpProbeServer::start().await.expect("start HTTP probe"); + let provider = GatingProvider::install_endpointless(progress) .await - .expect("install credentialed provider"); + .expect("install endpointless provider"); + (server, provider) +} - let result = async { - assert_rest_body_preserves_placeholder(&server).await?; - assert_websocket_binary_denied(&websocket_server).await - } - .await; +#[tokio::test] +async fn profile_rest_endpoint_keeps_body_placeholder() { + let progress = Progress::start("profile-rest"); + let (server, _websocket_server, provider) = profile_endpoint_fixture(progress).await; - cleanup_provider_resources().await; - result.expect("credential gating E2E"); + let output = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: false }, + CredentialSource::ProviderProfile, + ) + .await + .expect("run REST body client"); + assert!(output.contains("BODY_TEXT"), "{output}"); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(observations[0].saw_placeholder); + assert!(!observations[0].saw_secret); + progress.step("checks passed"); +} - install_endpointless_provider() +#[tokio::test] +async fn profile_websocket_endpoint_denies_binary_frames() { + let progress = Progress::start("profile-websocket"); + let (_server, websocket_server, provider) = profile_endpoint_fixture(progress).await; + + assert_websocket_binary_denied(progress, &provider.name, &websocket_server) .await - .expect("install endpointless provider"); - let endpointless_result = async { - assert_gateway_admission(server.port, CredentialSource::PolicyBinding).await?; - let literal = run_body_sandbox( - server.port, - EndpointMode::RestBody { rewrite: false }, - CredentialSource::PolicyBinding, - ) - .await?; - assert!(literal.contains("BODY_TEXT")); - let rewritten = run_body_sandbox( - server.port, - EndpointMode::RestBody { rewrite: true }, - CredentialSource::PolicyBinding, - ) - .await?; - assert!(rewritten.contains("BODY_REWRITTEN")); - assert!(!rewritten.contains(TEST_SECRET)); - assert!(!rewritten.contains(PLACEHOLDER_PREFIX)); - let observations = server.wait_for_observations(3).await; - assert_eq!(observations.len(), 3, "observations: {observations:?}"); - assert!(observations[1].saw_placeholder); - assert!(!observations[1].saw_secret); - assert!(!observations[2].saw_placeholder); - assert!(observations[2].saw_secret); - assert_endpointless_provider_env_live_update(server.port).await?; - Ok::<(), String>(()) - } - .await; - cleanup_provider_resources().await; - endpointless_result.expect("endpointless provider credential gating E2E"); + .expect("run WebSocket client"); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_requires_inspection_or_opt_in() { + let progress = Progress::start("binding-admission"); + let (server, provider) = endpointless_fixture(progress).await; + + assert_gateway_admission( + progress, + &provider.name, + server.port, + CredentialSource::PolicyBinding, + ) + .await + .expect("gateway admission"); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_rest_body_keeps_placeholder_without_rewrite() { + let progress = Progress::start("binding-body"); + let (server, provider) = endpointless_fixture(progress).await; + + let literal = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: false }, + CredentialSource::PolicyBinding, + ) + .await + .expect("run REST body client"); + assert!(literal.contains("BODY_TEXT"), "{literal}"); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(observations[0].saw_placeholder); + assert!(!observations[0].saw_secret); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_rest_body_rewrite_sends_secret_upstream() { + let progress = Progress::start("binding-rewrite"); + let (server, provider) = endpointless_fixture(progress).await; + + let rewritten = run_body_sandbox( + progress, + &provider.name, + server.port, + EndpointMode::RestBody { rewrite: true }, + CredentialSource::PolicyBinding, + ) + .await + .expect("run REST body client"); + assert!(rewritten.contains("BODY_REWRITTEN")); + assert!(!rewritten.contains(TEST_SECRET)); + assert!(!rewritten.contains(PLACEHOLDER_PREFIX)); + let observations = server.wait_for_observations(1).await; + assert_eq!(observations.len(), 1, "observations: {observations:?}"); + assert!(!observations[0].saw_placeholder); + assert!(observations[0].saw_secret); + progress.step("checks passed"); +} + +#[tokio::test] +async fn policy_binding_updates_provider_environment_live() { + let progress = Progress::start("binding-live-update"); + let (server, provider) = endpointless_fixture(progress).await; + + assert_endpointless_provider_env_live_update(progress, &provider.name, server.port) + .await + .expect("provider environment live update"); + progress.step("checks passed"); } From 53b8eec29cadab70e11be88776cf14a0fc6a7fdd Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:00:13 +0000 Subject: [PATCH 13/31] fix(providers): harden static credential placement (#4886) * fix(providers): enforce static credential request placement Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(providers): enforce placement using existing credential metadata Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(providers): default static credentials to Authorization placement Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * chore(proto): preserve frozen Go SDK bindings Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(providers): ignore placement fields unused by auth style Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-core/README.md | 19 + .../src/provider_credentials.rs | 754 ++++++++++++++++-- crates/openshell-core/src/secrets.rs | 529 +++++++++++- crates/openshell-server/README.md | 19 + crates/openshell-server/src/grpc/policy.rs | 40 +- crates/openshell-server/src/grpc/provider.rs | 220 +++++ crates/openshell-supervisor-network/README.md | 10 + .../src/l7/mod.rs | 31 + .../src/l7/relay.rs | 617 +++++++++++++- .../src/l7/rest.rs | 87 +- .../src/l7/websocket.rs | 101 ++- .../openshell-supervisor-network/src/proxy.rs | 8 +- .../src/test_support.rs | 23 +- .../src/config_runtime.rs | 29 +- docs/how-it-works/policies/network-rules.mdx | 5 +- docs/how-it-works/policies/schema.mdx | 4 +- docs/how-it-works/providers/overview.mdx | 28 +- docs/how-it-works/providers/profiles.mdx | 55 +- e2e/python/test_sandbox_providers.py | 159 ++++ providers/aws-s3.yaml | 2 + providers/aws.yaml | 2 + providers/codex.yaml | 7 + skills/debug-inference/SKILL.md | 3 +- skills/openshell-cli/SKILL.md | 13 + 24 files changed, 2610 insertions(+), 155 deletions(-) diff --git a/crates/openshell-core/README.md b/crates/openshell-core/README.md index d1ce03d653..b3d9a50349 100644 --- a/crates/openshell-core/README.md +++ b/crates/openshell-core/README.md @@ -58,6 +58,25 @@ router agree on provider defaults. Profiles define: Do not duplicate provider-specific inference behavior in callers. Add shared behavior here, then consume it from the gateway, sandbox, and router. +## Static Credential Resolution + +Static credential bindings retain endpoint authorization. The gateway includes +static `ProviderProfileCredential` auth metadata in the existing runtime +credential map under `static-env:`. Provider state extracts those entries +before storing dynamic token grants and compiles placement locally alongside the +matching static binding. Missing metadata defaults to the `Authorization` header; +malformed reserved metadata or an invalid selected placement fails closed +atomically. The same header default applies to request rewriting with standalone +resolver snapshots. +The credential's `auth_style` selects the placement; unused fields for other +styles are ignored and do not authorize additional locations. + +Request rewriting supplies the actual header name, query key, or path slot. +Header authorization is independent of authentication scheme, including Basic. +Explicit body/WebSocket rewrite policies and trusted signing retain their +existing permissions. Existing placeholder and stable-handle derivation remain +unchanged. Protobuf message shapes and service interfaces remain unchanged. + ## Middleware Contracts Built-in supervisor middleware identifiers, host-selector matching, and pure diff --git a/crates/openshell-core/src/provider_credentials.rs b/crates/openshell-core/src/provider_credentials.rs index d71f34b7c6..ab9bd26195 100644 --- a/crates/openshell-core/src/provider_credentials.rs +++ b/crates/openshell-core/src/provider_credentials.rs @@ -3,7 +3,7 @@ //! Runtime provider credential snapshots. -use crate::secrets::SecretResolver; +use crate::secrets::{CredentialPlacement, SecretResolver}; use crate::{ endpoint_path::EndpointPathPattern, host_pattern::HostPattern, @@ -67,6 +67,7 @@ struct StaticCredentialIdentityEpoch { #[derive(Debug, Clone)] struct CompiledStaticCredentialBinding { + placement: CredentialPlacement, endpoints: Vec, credential_identity: String, workload_credential_handle: String, @@ -97,6 +98,29 @@ impl fmt::Display for StaticCredentialBindingError { impl std::error::Error for StaticCredentialBindingError {} +/// Reserved entries in the existing credential map carry static placement only. +pub const STATIC_CREDENTIAL_METADATA_PREFIX: &str = "static-env:"; + +const CREDENTIAL_PATH_SLOT: &str = "{credential}"; + +fn split_static_credential_metadata( + credentials: HashMap, +) -> ( + HashMap, + HashMap, +) { + let mut metadata = HashMap::new(); + let mut grants = HashMap::new(); + for (key, credential) in credentials { + if let Some(env_key) = key.strip_prefix(STATIC_CREDENTIAL_METADATA_PREFIX) { + metadata.insert(env_key.to_string(), credential); + } else { + grants.insert(key, credential); + } + } + (metadata, grants) +} + impl ProviderCredentialState { pub fn from_environment( revision: u64, @@ -104,6 +128,7 @@ impl ProviderCredentialState { credential_expires_at_ms: HashMap, dynamic_credentials: HashMap, ) -> Self { + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); let (child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision( env, @@ -173,11 +198,14 @@ impl ProviderCredentialState { non_secret_environment_keys: Vec, stable_placeholder_environment_keys: Vec, ) -> Result { + let (placement_metadata, dynamic_credentials) = + split_static_credential_metadata(dynamic_credentials); let static_credential_bindings = compile_static_credential_bindings( &env, static_credential_bindings, &non_secret_environment_keys, &stable_placeholder_environment_keys, + &placement_metadata, )?; let stable_handles = static_credential_stable_handles(&static_credential_bindings); let (child_env, generation_resolver, current_resolver) = @@ -304,11 +332,21 @@ impl ProviderCredentialState { } pub fn resolver(&self) -> Option> { - self.inner + let inner = self + .inner .read() - .expect("provider credential state poisoned") - .combined_resolver - .clone() + .expect("provider credential state poisoned"); + inner.combined_resolver.as_ref().map(|resolver| { + Arc::new( + resolver.as_ref().clone().with_placements( + inner + .static_credential_bindings + .iter() + .map(|(key, binding)| (key.clone(), binding.placement.clone())) + .collect(), + ), + ) + }) } /// Resolve provider placeholders only for credentials bound to this @@ -414,11 +452,23 @@ impl ProviderCredentialState { }) .map(|(key, epoch)| (key.clone(), epoch.revisions.clone())) .collect(); - Arc::new(resolver.scoped_to_env_keys( - &inner.known_static_credential_keys, - &allowed, - revision_fallback_allowed_revisions, - )) + Arc::new( + resolver + .as_ref() + .clone() + .with_placements( + inner + .static_credential_bindings + .iter() + .map(|(key, binding)| (key.clone(), binding.placement.clone())) + .collect(), + ) + .scoped_to_env_keys( + &inner.known_static_credential_keys, + &allowed, + revision_fallback_allowed_revisions, + ), + ) }); (resolver, classifier, revision) } @@ -694,6 +744,7 @@ impl ProviderCredentialState { credential_expires_at_ms: HashMap, dynamic_credentials: HashMap, ) -> usize { + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); let (mut child_env, generation_resolver, current_resolver) = SecretResolver::from_provider_env_for_current_revision( env, @@ -774,11 +825,14 @@ impl ProviderCredentialState { non_secret_environment_keys: Vec, stable_placeholder_environment_keys: Vec, ) -> Result { + let (placement_metadata, dynamic_credentials) = + split_static_credential_metadata(dynamic_credentials); let static_credential_bindings = match compile_static_credential_bindings( &env, static_credential_bindings, &non_secret_environment_keys, &stable_placeholder_environment_keys, + &placement_metadata, ) { Ok(bindings) => bindings, Err(error) => { @@ -939,6 +993,7 @@ impl ProviderCredentialState { .expect("provider credential state poisoned"); let dynamic_credentials = dynamic_credentials.unwrap_or_else(|| inner.current.dynamic_credentials.clone()); + let (_, dynamic_credentials) = split_static_credential_metadata(dynamic_credentials); inner.current = Arc::new(ProviderCredentialSnapshot { installation_id: uuid::Uuid::new_v4().to_string(), revision, @@ -959,7 +1014,22 @@ fn compile_static_credential_bindings( bindings: HashMap, non_secret_environment_keys: &[String], stable_placeholder_environment_keys: &[String], + placement_metadata: &HashMap, ) -> Result, StaticCredentialBindingError> { + for (key, metadata) in placement_metadata { + if !env.contains_key(key) + || !bindings.contains_key(key) + || metadata.env_vars.len() != 1 + || metadata.env_vars[0] != *key + || metadata.token_grant.is_some() + || metadata.refresh.is_some() + || !metadata.token_grant_owners.is_empty() + { + return Err(binding_error( + "static credential placement metadata does not match delivered credentials", + )); + } + } let non_secret_keys = non_secret_environment_keys .iter() .cloned() @@ -1056,6 +1126,10 @@ fn compile_static_credential_bindings( bindings .into_iter() .map(|(key, binding)| { + let placement = placement_metadata.get(&key).map_or_else( + || Ok(CredentialPlacement::Header("authorization".into())), + compile_credential_placement, + )?; let endpoints = binding .endpoints .into_iter() @@ -1065,6 +1139,7 @@ fn compile_static_credential_bindings( key, CompiledStaticCredentialBinding { endpoints, + placement, credential_identity: binding.credential_identity, workload_credential_handle: binding.workload_credential_handle, }, @@ -1073,6 +1148,52 @@ fn compile_static_credential_bindings( .collect() } +fn compile_credential_placement( + metadata: &crate::proto::ProviderProfileCredential, +) -> Result { + let style = metadata.auth_style.trim().to_ascii_lowercase(); + // Authored profiles may retain fields for other auth styles. Only the + // selected style grants a location; unused fields grant no authority. + let placement = match style.as_str() { + "basic" => CredentialPlacement::Header("authorization".into()), + "header" | "bearer" + if !metadata.header_name.trim().is_empty() + && metadata + .header_name + .trim() + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)) => + { + CredentialPlacement::Header(metadata.header_name.trim().to_ascii_lowercase()) + } + "query" + if !metadata.query_param.trim().is_empty() + && !metadata + .query_param + .chars() + .any(|c| c.is_control() || "&=#?".contains(c)) => + { + CredentialPlacement::QueryParam(metadata.query_param.trim().to_string()) + } + "path" + if metadata.path_template.starts_with('/') + && metadata.path_template.matches(CREDENTIAL_PATH_SLOT).count() == 1 + && !metadata + .path_template + .chars() + .any(|c| c.is_control() || "?#".contains(c)) => + { + CredentialPlacement::PathTemplate(metadata.path_template.clone()) + } + _ => { + return Err(binding_error( + "static credential placement metadata has an invalid request location", + )); + } + }; + Ok(placement) +} + fn compile_static_credential_endpoint( endpoint: StaticCredentialEndpointBinding, ) -> Result { @@ -1346,6 +1467,10 @@ mod tests { ); } + fn authorization_location() -> crate::secrets::RequestCredentialLocation { + crate::secrets::RequestCredentialLocation::Header("Authorization".into()) + } + fn binding(host: &str, port: u32, path: &str) -> StaticCredentialBinding { StaticCredentialBinding { endpoints: vec![StaticCredentialEndpointBinding { @@ -1365,15 +1490,336 @@ mod tests { binding } + fn placement_metadata(style: &str) -> HashMap { + HashMap::from([( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}API_KEY"), + crate::proto::ProviderProfileCredential { + name: "api_key".into(), + env_vars: vec!["API_KEY".into()], + auth_style: style.into(), + header_name: if matches!(style, "header" | "bearer" | "basic") { + "Authorization".into() + } else { + String::new() + }, + query_param: if style == "query" { + "api_key".into() + } else { + String::new() + }, + path_template: if style == "path" { + "/v1/{credential}/events".into() + } else { + String::new() + }, + ..Default::default() + }, + )]) + } + + #[test] + fn metadata_free_static_binding_defaults_to_authorization_across_rotation() { + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "initial".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + for revision in [7, 8] { + if revision == 8 { + state + .install_bound_environment( + revision, + HashMap::from([("API_KEY".into(), "rotated".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + } + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/search") + .unwrap(); + assert!(resolver.resolve_placeholder(&reference).is_none()); + for token in [ + reference.clone(), + format!("vendor-OPENSHELL-RESOLVE-ENV-v{revision}_API_KEY"), + jwt_alias(&reference), + ] { + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Bearer {token}")) + .unwrap() + .is_some() + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &token) + .unwrap_err() + .is_placement_mismatch() + ); + for target in [format!("/search?token={token}"), format!("/api/{token}")] { + assert!(crate::secrets::rewrite_target_for_eval(&target, &resolver).is_err()); + } + } + let current = &state.snapshot().child_env["API_KEY"]; + assert_eq!( + resolver + .rewrite_header_value_for_name("Authorization", current) + .unwrap(), + Some(if revision == 7 { "initial" } else { "rotated" }.into()) + ); + } + } + + #[test] + fn explicit_custom_header_overrides_authorization_default() { + let mut metadata = placement_metadata("header"); + metadata.values_mut().next().unwrap().header_name = "x-api-key".into(); + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + metadata, + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/search") + .unwrap(); + assert_eq!( + resolver + .rewrite_header_value_for_name("X-Api-Key", &reference) + .unwrap(), + Some("synthetic".into()) + ); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &reference) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[test] + fn static_metadata_selects_one_location_and_preserves_other_providers() { + for style in ["bearer", "header", "basic", "query", "path"] { + let env = HashMap::from([ + ("API_KEY".into(), "synthetic".into()), + ("OTHER_KEY".into(), "other-secret".into()), + ]); + let mut other_binding = binding("other.example.com", 443, "/**"); + other_binding.credential_identity = "provider-b:OTHER_KEY".into(); + let bindings = HashMap::from([ + ("API_KEY".into(), binding("api.example.com", 443, "/**")), + ("OTHER_KEY".into(), other_binding), + ]); + let state = ProviderCredentialState::from_bound_environment( + 7, + env.clone(), + HashMap::new(), + HashMap::new(), + bindings.clone(), + vec![], + ) + .unwrap(); + // Older documented profiles populated all three placement fields. + // Only auth_style's selected field may grant request authority. + let mut metadata = placement_metadata(style); + let declaration = metadata.values_mut().next().unwrap(); + declaration.header_name = if style == "basic" { + "x-unused-for-basic" + } else { + "Authorization" + } + .into(); + declaration.query_param = "api_key".into(); + declaration.path_template = "/v1/{credential}/events".into(); + assert_eq!( + state + .install_bound_environment(8, env, HashMap::new(), metadata, bindings, vec![]) + .unwrap(), + 2 + ); + assert!(state.snapshot().dynamic_credentials.is_empty()); + assert_eq!( + state + .resolver_for_endpoint("other.example.com", 443, "/") + .unwrap() + .rewrite_header_value_for_name( + "Authorization", + &state.snapshot().child_env["OTHER_KEY"], + ) + .unwrap(), + Some("other-secret".into()) + ); + let reference = state.snapshot().child_env["API_KEY"].clone(); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/chat") + .unwrap(); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &reference) + .is_err() + ); + let header = resolver.rewrite_header_value_for_name("Authorization", &reference); + let query = crate::secrets::rewrite_target_for_eval( + &format!("/v1/chat?api_key={reference}"), + &resolver, + ); + let path = crate::secrets::rewrite_target_for_eval( + &format!("/v1/{reference}/events"), + &resolver, + ); + assert_eq!( + header.is_ok(), + matches!(style, "bearer" | "header" | "basic") + ); + assert_eq!(query.is_ok(), style == "query"); + assert_eq!(path.is_ok(), style == "path"); + } + } + + #[test] + fn malformed_static_metadata_revokes_static_material_and_preserves_real_grants() { + for malformed in 0..11 { + let state = ProviderCredentialState::from_bound_environment( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + HashMap::new(), + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .unwrap(); + let mut metadata = placement_metadata("header"); + let key = format!("{STATIC_CREDENTIAL_METADATA_PREFIX}API_KEY"); + let value = metadata.get_mut(&key).unwrap(); + match malformed { + 0 => value.env_vars = vec!["OTHER".into()], + 1 => { + value.token_grant = Some(crate::proto::ProviderCredentialTokenGrant::default()); + } + 2 => value.refresh = Some(crate::proto::ProviderCredentialRefresh::default()), + 3 => value.token_grant_owners = vec!["owner".into()], + 4 => value.auth_style = "unsupported".into(), + 5 => { + let value = metadata.remove(&key).unwrap(); + metadata.insert(format!("{STATIC_CREDENTIAL_METADATA_PREFIX}OTHER"), value); + } + 6 => value.header_name.clear(), + 7 => value.header_name = "Bad Header".into(), + 8 => { + value.auth_style = "path".into(); + value.header_name.clear(); + value.path_template = "/no-slot".into(); + } + 9 => { + value.auth_style = "query".into(); + value.query_param = "api?key".into(); + } + 10 => value.auth_style.clear(), + _ => unreachable!(), + } + metadata.insert( + "real-grant".into(), + crate::proto::ProviderProfileCredential { + token_grant: Some(crate::proto::ProviderCredentialTokenGrant::default()), + ..Default::default() + }, + ); + assert!( + state + .install_bound_environment( + 8, + HashMap::from([("API_KEY".into(), "replacement".into())]), + HashMap::new(), + metadata, + HashMap::from([("API_KEY".into(), binding("api.example.com", 443, "/**"))]), + vec![], + ) + .is_err() + ); + assert!(state.snapshot().child_env.is_empty()); + assert_eq!(state.snapshot().dynamic_credentials.len(), 1); + assert!( + state + .snapshot() + .dynamic_credentials + .contains_key("real-grant") + ); + } + } + + #[test] + fn stable_references_survive_metadata_arrival_and_use_the_current_location() { + let handle = "a".repeat(64); + let bindings = HashMap::from([( + "API_KEY".into(), + stable_binding("api.example.com", 443, "/**", &handle), + )]); + let state = ProviderCredentialState::from_bound_environment_with_stable_placeholders( + 7, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + HashMap::new(), + bindings.clone(), + vec![], + vec!["API_KEY".into()], + ) + .unwrap(); + let reference = state.snapshot().child_env["API_KEY"].clone(); + for style in [None, Some("header"), Some("query"), None] { + state + .install_bound_environment_with_stable_placeholders( + 8, + HashMap::from([("API_KEY".into(), "synthetic".into())]), + HashMap::new(), + style.map_or_else(HashMap::new, placement_metadata), + bindings.clone(), + vec![], + vec!["API_KEY".into()], + ) + .unwrap(); + assert_eq!(state.snapshot().child_env["API_KEY"], reference); + let resolver = state + .resolver_for_endpoint("api.example.com", 443, "/v1/chat") + .unwrap(); + let query = crate::secrets::rewrite_target_for_eval( + &format!("/v1/chat?api_key={reference}"), + &resolver, + ); + assert_eq!(query.is_ok(), style == Some("query")); + assert_eq!( + resolver + .rewrite_header_value_for_name("Authorization", &reference) + .is_ok(), + style != Some("query") + ); + } + } + fn assert_binding_validation_error( env: HashMap, bindings: HashMap, non_secret_environment_keys: Vec, expected: &str, ) { - let error = - compile_static_credential_bindings(&env, bindings, &non_secret_environment_keys, &[]) - .expect_err("malformed provider metadata must fail validation"); + let error = compile_static_credential_bindings( + &env, + bindings, + &non_secret_environment_keys, + &[], + &HashMap::new(), + ) + .expect_err("malformed provider metadata must fail validation"); assert_eq!(error.to_string(), expected); } @@ -1472,7 +1918,12 @@ mod tests { let allowed = state .resolver_for_endpoint("api.example.com", 443, "/v1/messages?stream=true") .expect("resolver"); - assert_eq!(allowed.resolve_placeholder(placeholder), Some("secret")); + assert_eq!( + allowed + .with_request_location(authorization_location()) + .resolve_placeholder(placeholder), + Some("secret") + ); for (host, port, path) in [ ("example.com", 443, "/v1/messages"), @@ -1483,7 +1934,7 @@ mod tests { .resolver_for_endpoint(host, port, path) .expect("resolver"); let error = denied - .rewrite_header_value(placeholder) + .rewrite_header_value_for_name("Authorization", placeholder) .expect_err("endpoint mismatch must fail closed"); assert!(error.is_endpoint_mismatch(), "{host}:{port}{path}"); } @@ -1522,16 +1973,22 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("endpoint resolver"); assert_eq!( - resolver.resolve_placeholder(&stable_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), None, "ordinary endpoint-bound credentials remain revision scoped" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_ORDINARY_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_ORDINARY_TOKEN"), Some("ordinary") ); @@ -1559,16 +2016,22 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("refreshed endpoint resolver"); assert_eq!( - refreshed.resolve_placeholder(&stable_placeholder), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), Some("new") ); assert_eq!( - refreshed.resolve_placeholder("openshell:resolve:env:EXTERNAL_ASSERTION"), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:EXTERNAL_ASSERTION"), None, "an identityless current-name alias must remain unavailable" ); assert_eq!( - refreshed.resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), + refreshed + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:ORDINARY_TOKEN"), None ); @@ -1591,12 +2054,16 @@ mod tests { .resolver_for_endpoint("tools.example.com", 443, "/mcp/local") .expect("replacement endpoint resolver"); assert_eq!( - replacement.resolve_placeholder(&stable_placeholder), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder(&stable_placeholder), None, "a placeholder issued for an earlier provider identity must be revoked" ); assert_eq!( - replacement.resolve_placeholder(&state.snapshot().child_env["EXTERNAL_ASSERTION"]), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder(&state.snapshot().child_env["EXTERNAL_ASSERTION"]), Some("replacement") ); } @@ -1650,16 +2117,20 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("authorized endpoint resolver"); assert!( - resolver.resolve_placeholder(&original) + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) == Some(format!("synthetic-{revision}").as_str()) ); assert!( resolver + .with_request_location(authorization_location()) .resolve_placeholder("openshell:resolve:env:API_KEY") .is_none() ); assert!( resolver + .with_request_location(authorization_location()) .resolve_placeholder("openshell:resolve:env:v1_API_KEY") .is_none() ); @@ -1681,6 +2152,7 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("reconstructed resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&original) == Some("synthetic-13") ); @@ -1692,7 +2164,11 @@ mod tests { let resolver = reconstructed .resolver_for_endpoint(host, port, path) .expect("endpoint-scoped resolver"); - assert!(resolver.rewrite_header_value(&original).is_err()); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &original) + .is_err() + ); } // Current-value expiry must not fall back to a previously valid value. @@ -1710,7 +2186,12 @@ mod tests { let resolver = reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") .expect("expired resolver"); - assert!(resolver.resolve_placeholder(&original).is_none()); + assert!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) + .is_none() + ); // An activated detach removes the current binding and all authority for // a still-running process, even when it retains its original reference. @@ -1729,7 +2210,10 @@ mod tests { assert!( state .resolver_for_endpoint("api.example.com", 443, "/v1/chat/completions") - .is_none_or(|resolver| resolver.resolve_placeholder(&original).is_none()) + .is_none_or(|resolver| resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&original) + .is_none()) ); } @@ -1856,11 +2340,15 @@ mod tests { .resolver_for_endpoint("a.example.com", 443, "/a/check") .expect("endpoint A resolver"); assert_eq!( - resolver_a.resolve_placeholder("openshell:resolve:env:v7_KEY_A"), + resolver_a + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_A"), Some("secret-a") ); assert_eq!( - resolver_a.resolve_placeholder("openshell:resolve:env:v7_KEY_B"), + resolver_a + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_B"), None, "credential B must not resolve at endpoint A" ); @@ -1869,11 +2357,15 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/b/check") .expect("endpoint B resolver"); assert_eq!( - resolver_b.resolve_placeholder("openshell:resolve:env:v7_KEY_B"), + resolver_b + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_B"), Some("secret-b") ); assert_eq!( - resolver_b.resolve_placeholder("openshell:resolve:env:v7_KEY_A"), + resolver_b + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_KEY_A"), None, "credential A must not resolve at endpoint B" ); @@ -1912,7 +2404,9 @@ mod tests { .resolver_for_endpoint("new.example.com", 443, "/v2/messages") .expect("replacement endpoint resolver"); assert_eq!( - replacement.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + replacement + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), Some("new") ); @@ -1920,7 +2414,7 @@ mod tests { .resolver_for_endpoint("old.example.com", 443, "/v1/messages") .expect("restricted resolver"); let error = removed - .rewrite_header_value("openshell:resolve:env:v2_API_KEY") + .rewrite_header_value_for_name("Authorization", "openshell:resolve:env:v2_API_KEY") .expect_err("replaced endpoint binding must no longer authorize the credential"); assert!(error.is_endpoint_mismatch()); } @@ -1948,7 +2442,12 @@ mod tests { &format!("/bot{placeholder}/sendMessage?stream=true"), ) .expect("syntax-redacted path should select the binding"); - assert_eq!(resolver.resolve_placeholder(placeholder), Some("secret")); + assert_eq!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(placeholder), + Some("secret") + ); } #[test] @@ -1974,7 +2473,9 @@ mod tests { ) .expect("syntax-redacted alias path should select the binding"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v7_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v7_API_KEY"), Some("secret") ); } @@ -2021,7 +2522,9 @@ mod tests { .resolver_for_endpoint("unrelated.example", 1234, "/") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v3_GCP_PROJECT_ID"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v3_GCP_PROJECT_ID"), Some("project") ); } @@ -2076,7 +2579,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("recovered"), "a metadata failure must not permanently strand placeholders from the same provider identity" ); @@ -2128,7 +2633,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("new"), "the running process placeholder should recover against the current same-identity secret" ); @@ -2167,11 +2674,15 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), Some("new") ); } @@ -2202,6 +2713,7 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), None, "the expired access token must fail closed" @@ -2210,7 +2722,10 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .is_err(), "wrapping a placeholder must not bypass access-token expiry" ); @@ -2231,17 +2746,24 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder(&workload_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&workload_placeholder), Some("current") ); assert_eq!( resolver - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .expect("refresh updates the JWT alias"), Some("Bearer current".to_string()) ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v2_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v2_API_KEY"), None, "refresh-managed credentials must not expose revision aliases" ); @@ -2283,6 +2805,7 @@ mod tests { state .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), Some(token.as_str()) ); @@ -2315,6 +2838,7 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") + .with_request_location(authorization_location()) .resolve_placeholder(&workload_placeholder), Some("token-14") ); @@ -2322,7 +2846,10 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1") .expect("resolver") - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&workload_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&workload_placeholder)) + ) .expect("the JWT alias survives supervisor reconstruction"), Some("Bearer token-14".to_string()) ); @@ -2363,10 +2890,18 @@ mod tests { let current = state .resolver_for_endpoint("new.example.com", 443, "/v2/messages") .expect("current resolver"); - assert_eq!(current.resolve_placeholder(&old_placeholder), None); + assert_eq!( + current + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder), + None + ); assert!( current - .rewrite_header_value(&format!("Bearer {}", jwt_alias(&old_placeholder))) + .rewrite_header_value_for_name( + "Authorization", + &format!("Bearer {}", jwt_alias(&old_placeholder)) + ) .is_err(), "a JWT alias must not carry a revoked identity into the new epoch" ); @@ -2377,27 +2912,32 @@ mod tests { ] { assert!( current - .rewrite_header_value(&format!("Bearer {alias}")) + .rewrite_header_value_for_name("Authorization", &format!("Bearer {alias}")) .is_err(), "identityless and malformed JWT aliases must fail closed" ); } assert_eq!( - current.resolve_placeholder(&state.snapshot().child_env["API_KEY"]), + current + .with_request_location(authorization_location()) + .resolve_placeholder(&state.snapshot().child_env["API_KEY"]), Some("new") ); let wrong_endpoint = state .resolver_for_endpoint("old.example.com", 443, "/v1/messages") .expect("endpoint-scoped resolver"); let error = wrong_endpoint - .rewrite_header_value(&state.snapshot().child_env["API_KEY"]) + .rewrite_header_value_for_name("Authorization", &state.snapshot().child_env["API_KEY"]) .expect_err("new handle must not resolve at the old endpoint"); assert!(error.is_endpoint_mismatch()); let error = wrong_endpoint - .rewrite_header_value(&format!( - "Bearer {}", - jwt_alias(&state.snapshot().child_env["API_KEY"]) - )) + .rewrite_header_value_for_name( + "Authorization", + &format!( + "Bearer {}", + jwt_alias(&state.snapshot().child_env["API_KEY"]) + ), + ) .expect_err("JWT wrapping preserves endpoint authorization"); assert!(error.is_endpoint_mismatch()); } @@ -2442,7 +2982,9 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/v2/inference") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v50_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v50_API_KEY"), Some("secret-6"), "an aged-out placeholder may use the current secret across revisions in both numeric directions while its provider identity is unchanged" ); @@ -2484,26 +3026,35 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)), None, "an opaque revision from another provider identity must fail closed even when it is numerically greater" ); assert!( resolver - .rewrite_header_value(&format!( - "Bearer {}", - jwt_alias(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)) - )) + .rewrite_header_value_for_name( + "Authorization", + &format!( + "Bearer {}", + jwt_alias(&format!("openshell:resolve:env:v{}_API_KEY", u64::MAX)) + ) + ) .is_err(), "a JWT alias must not resolve a replacement provider's credential" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:API_KEY"), None, "an identityless canonical placeholder must not resolve a replacement provider" ); assert_eq!( - resolver.resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), None, "an identityless provider alias must not resolve a replacement provider" ); @@ -2546,17 +3097,23 @@ mod tests { .resolver_for_endpoint("b.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), None, "a placeholder issued before detach must not resolve to a replacement provider" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:API_KEY"), None, "an identityless canonical placeholder must not cross a failed refresh into a replacement identity" ); assert_eq!( - resolver.resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("vendor-OPENSHELL-RESOLVE-ENV-API_KEY"), None, "an identityless provider alias must not cross a failed refresh into a replacement identity" ); @@ -2600,12 +3157,16 @@ mod tests { .resolver_for_endpoint("a.example.com", 443, "/v1") .expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v1_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v1_API_KEY"), None, "a successful empty environment represents detach and must invalidate old membership" ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v4_API_KEY"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v4_API_KEY"), Some("reattached") ); } @@ -2638,19 +3199,27 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), Some("new") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), Some("new") ); assert_eq!( - resolver.resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), Some("new") ); } @@ -2669,15 +3238,21 @@ mod tests { assert!(state.snapshot().child_env.is_empty()); let resolver = state.resolver().expect("old resolver retained"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("old") ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:GITHUB_TOKEN"), None ); assert_eq!( - resolver.resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("provider-OPENSHELL-RESOLVE-ENV-GITHUB_TOKEN"), None ); } @@ -2707,11 +3282,15 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), None ); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v11_GITHUB_TOKEN"), Some("new") ); } @@ -3075,11 +3654,15 @@ mod tests { .resolver_for_endpoint("api.example.com", 443, "/") .unwrap(); assert_eq!( - resolver.resolve_placeholder(&old_placeholder), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder), Some("first-secret") ); assert_eq!( - resolver.resolve_placeholder(&live.snapshot().child_env["API_KEY"]), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&live.snapshot().child_env["API_KEY"]), Some("second-secret"), ); } @@ -3138,7 +3721,12 @@ mod tests { let resolver = observer .resolver_for_endpoint("api.example.com", 443, "/") .unwrap(); - assert!(resolver.resolve_placeholder(&old_placeholder).is_none()); + assert!( + resolver + .with_request_location(authorization_location()) + .resolve_placeholder(&old_placeholder) + .is_none() + ); } #[test] @@ -3303,7 +3891,9 @@ mod tests { let resolver = state.resolver().expect("resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), Some("new-19") ); } @@ -3328,7 +3918,9 @@ mod tests { let resolver = state.resolver().expect("retained resolver"); assert_eq!( - resolver.resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), + resolver + .with_request_location(authorization_location()) + .resolve_placeholder("openshell:resolve:env:v10_GITHUB_TOKEN"), None ); } diff --git a/crates/openshell-core/src/secrets.rs b/crates/openshell-core/src/secrets.rs index 2e51c9de28..6f4856be4c 100644 --- a/crates/openshell-core/src/secrets.rs +++ b/crates/openshell-core/src/secrets.rs @@ -114,9 +114,18 @@ pub struct UnresolvedPlaceholderError { enum UnresolvedPlaceholderReason { Unavailable, EndpointMismatch, + PlacementMismatch, } +pub const CREDENTIAL_PLACEMENT_MISMATCH: &str = + "credential is not authorized for the request location"; + impl UnresolvedPlaceholderError { + #[must_use] + pub fn is_placement_mismatch(&self) -> bool { + self.reason == UnresolvedPlaceholderReason::PlacementMismatch + } + #[must_use] pub fn unavailable(location: &'static str) -> Self { unresolved(location) @@ -136,6 +145,7 @@ impl fmt::Display for UnresolvedPlaceholderError { match self.reason { UnresolvedPlaceholderReason::Unavailable => "credential placeholder could not be resolved", + UnresolvedPlaceholderReason::PlacementMismatch => CREDENTIAL_PLACEMENT_MISMATCH, UnresolvedPlaceholderReason::EndpointMismatch => "credential is not authorized for the request endpoint", }, @@ -180,9 +190,31 @@ pub struct RewriteTargetResult { // SecretResolver // --------------------------------------------------------------------------- +/// Credential-specific destinations authorized by the provider profile. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CredentialPlacement { + Header(String), + QueryParam(String), + PathTemplate(String), +} + +/// The actual destination of a workload-provided credential placeholder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RequestCredentialLocation { + Header(String), + QueryParam(String), + Path(String), + Body, + WebSocket, +} + #[derive(Clone, Default)] pub struct SecretResolver { by_placeholder: HashMap, + // Enabled only by the secret-free policy projection below, never by runtime callers. + policy_redaction_only: bool, + placements: HashMap, + request_location: Option, denied_env_keys: HashSet, identity_bound_env_keys: HashSet, revision_fallback_allowed_revisions: HashMap>>, @@ -292,6 +324,9 @@ impl SecretResolver { let resolver = |by_placeholder: HashMap| { (!by_placeholder.is_empty()).then_some(Self { by_placeholder, + policy_redaction_only: false, + placements: HashMap::new(), + request_location: None, denied_env_keys: HashSet::new(), identity_bound_env_keys: HashSet::new(), revision_fallback_allowed_revisions: HashMap::new(), @@ -347,6 +382,9 @@ impl SecretResolver { child_env, Some(Self { by_placeholder, + policy_redaction_only: false, + placements: HashMap::new(), + request_location: None, denied_env_keys: HashSet::new(), identity_bound_env_keys: HashSet::new(), revision_fallback_allowed_revisions: HashMap::new(), @@ -357,11 +395,13 @@ impl SecretResolver { pub fn merge<'a>(resolvers: impl IntoIterator) -> Option { let mut by_placeholder = HashMap::new(); + let mut placements = HashMap::new(); let mut denied_env_keys = HashSet::new(); let mut identity_bound_env_keys = HashSet::new(); let mut revision_fallback_allowed_revisions = HashMap::new(); for resolver in resolvers { by_placeholder.extend(resolver.by_placeholder.clone()); + placements.extend(resolver.placements.clone()); denied_env_keys.extend(resolver.denied_env_keys.iter().cloned()); identity_bound_env_keys.extend(resolver.identity_bound_env_keys.iter().cloned()); revision_fallback_allowed_revisions @@ -372,6 +412,9 @@ impl SecretResolver { } else { Some(Self { by_placeholder, + policy_redaction_only: false, + placements, + request_location: None, denied_env_keys, identity_bound_env_keys, revision_fallback_allowed_revisions, @@ -405,12 +448,104 @@ impl SecretResolver { .collect(); Self { by_placeholder, + policy_redaction_only: self.policy_redaction_only, + placements: self.placements.clone(), + request_location: None, denied_env_keys, identity_bound_env_keys: bound_keys.clone(), revision_fallback_allowed_revisions, } } + /// Attach mandatory placement metadata to a resolver snapshot. + pub(crate) fn with_placements( + mut self, + placements: HashMap, + ) -> Self { + self.placements = placements; + self + } + + /// Select the actual structured request destination before resolving input. + #[must_use] + pub fn with_request_location(&self, location: RequestCredentialLocation) -> Self { + let mut scoped = self.clone(); + scoped.request_location = Some(location); + scoped + } + + fn placement_permits(&self, placeholder: &str) -> bool { + if self.policy_redaction_only { + return true; + } + let Some(key) = placeholder_env_key(placeholder) else { + return true; + }; + // Context-free reads are trusted configuration/expiry lookups only. + // Bound credentials still require an actual request destination. + if self.request_location.is_none() + && !self.identity_bound_env_keys.contains(key) + && !self.placements.contains_key(key) + { + return true; + } + if matches!( + self.request_location, + Some(RequestCredentialLocation::Body | RequestCredentialLocation::WebSocket) + ) { + // Only explicit endpoint-policy rewrite wrappers choose these contexts. + return true; + } + let Some(placement) = self.placements.get(key) else { + return matches!(self.request_location.as_ref(), Some(RequestCredentialLocation::Header(name)) if name.eq_ignore_ascii_case("authorization")); + }; + match (placement, self.request_location.as_ref()) { + ( + CredentialPlacement::Header(expected), + Some(RequestCredentialLocation::Header(actual)), + ) => expected.eq_ignore_ascii_case(actual), + ( + CredentialPlacement::QueryParam(expected), + Some(RequestCredentialLocation::QueryParam(actual)), + ) => expected == actual, + ( + CredentialPlacement::PathTemplate(expected), + Some(RequestCredentialLocation::Path(actual)), + ) => { + // Normalize only this credential's slot; all other path text must match. + let mut normalized = String::new(); + let mut pos = 0; + let mut slots = 0; + while pos < actual.len() { + let next_canonical = actual[pos..] + .find(PLACEHOLDER_PREFIX) + .map(|offset| pos + offset); + let next_alias = actual[pos..] + .find(PROVIDER_ALIAS_MARKER) + .map(|offset| alias_start_for_marker(actual, pos + offset)); + let next = [next_canonical, next_alias].into_iter().flatten().min(); + let Some(start) = next else { + normalized.push_str(&actual[pos..]); + break; + }; + normalized.push_str(&actual[pos..start]); + let Some((end, token)) = self.credential_token_at(actual, start) else { + return false; + }; + if placeholder_env_key(token) == Some(key) { + normalized.push_str("{credential}"); + slots += 1; + } else { + normalized.push_str(token); + } + pos = end; + } + slots == 1 && normalized == *expected + } + _ => false, + } + } + fn unresolved_for( &self, location: &'static str, @@ -420,6 +555,8 @@ impl SecretResolver { .is_some_and(|key| self.denied_env_keys.contains(key)) { UnresolvedPlaceholderReason::EndpointMismatch + } else if !self.placement_permits(placeholder) { + UnresolvedPlaceholderReason::PlacementMismatch } else { UnresolvedPlaceholderReason::Unavailable }; @@ -435,6 +572,9 @@ impl SecretResolver { // handle retains the original endpoint, epoch, and expiration constraints. let canonical_alias = alias_env_key(value).map(placeholder_for_env_key); let value = canonical_alias.as_deref().unwrap_or(value); + if !self.placement_permits(value) { + return None; + } if placeholder_env_key(value).is_some_and(|key| self.identity_bound_env_keys.contains(key)) && revisioned_placeholder_parts(value).is_none() && stable_placeholder_parts(value).is_none() @@ -510,6 +650,9 @@ impl SecretResolver { reason: UnresolvedPlaceholderReason::EndpointMismatch, }); } + if !self.placement_permits(value) { + return Err(self.unresolved_for(location, value)); + } Ok(self.resolve_placeholder(value)) } @@ -519,6 +662,15 @@ impl SecretResolver { .map(|s| s.expires_at_ms) } + pub fn rewrite_header_value_for_name( + &self, + name: &str, + value: &str, + ) -> Result, UnresolvedPlaceholderError> { + self.with_request_location(RequestCredentialLocation::Header(name.to_string())) + .rewrite_header_value(value) + } + pub fn rewrite_header_value( &self, value: &str, @@ -562,6 +714,15 @@ impl SecretResolver { &self, text: &mut String, location: &'static str, + ) -> Result { + self.with_request_location(RequestCredentialLocation::Body) + .rewrite_text_placeholders_at_location(text, location) + } + + fn rewrite_text_placeholders_at_location( + &self, + text: &mut String, + location: &'static str, ) -> Result { if !contains_raw_reserved_marker(text) { return Ok(0); @@ -627,7 +788,8 @@ impl SecretResolver { &self, text: &mut String, ) -> Result { - self.rewrite_text_placeholders(text, "websocket") + self.with_request_location(RequestCredentialLocation::WebSocket) + .rewrite_text_placeholders_at_location(text, "websocket") } fn credential_token_at<'a>( @@ -677,7 +839,7 @@ impl SecretResolver { } let mut rewritten = decoded; - let replacements = self.rewrite_text_placeholders(&mut rewritten, "header")?; + let replacements = self.rewrite_text_placeholders_at_location(&mut rewritten, "header")?; if replacements == 0 { return Ok(None); @@ -1092,6 +1254,9 @@ fn rewrite_uri_path( path: &str, resolver: &SecretResolver, ) -> Result, UnresolvedPlaceholderError> { + let scoped = + resolver.with_request_location(RequestCredentialLocation::Path(percent_decode(path))); + let resolver = &scoped; let segments: Vec<&str> = path.split('/').collect(); let mut resolved_segments = Vec::with_capacity(segments.len()); let mut redacted_segments = Vec::with_capacity(segments.len()); @@ -1202,8 +1367,11 @@ fn rewrite_uri_query_params( let decoded_value = percent_decode(value); if contains_raw_reserved_marker(&decoded_value) { let mut rewritten = decoded_value.clone(); + let decoded_key = percent_decode(&key.replace('+', " ")); + let scoped = resolver + .with_request_location(RequestCredentialLocation::QueryParam(decoded_key)); let replacements = - resolver.rewrite_text_placeholders(&mut rewritten, "query_param")?; + scoped.rewrite_text_placeholders_at_location(&mut rewritten, "query_param")?; if replacements == 0 || contains_raw_reserved_marker(&rewritten) { return Err(unresolved("query_param")); } @@ -1326,10 +1494,12 @@ pub fn rewrite_header_line_checked( return Ok(line.to_string()); }; - resolver.rewrite_header_value(value.trim())?.map_or_else( - || Ok(line.to_string()), - |rewritten| Ok(format!("{name}: {rewritten}")), - ) + resolver + .rewrite_header_value_for_name(name, value.trim())? + .map_or_else( + || Ok(line.to_string()), + |rewritten| Ok(format!("{name}: {rewritten}")), + ) } /// Resolve placeholders in a request target (path + query) for OPA evaluation. @@ -1418,9 +1588,10 @@ pub fn redact_target_for_policy(target: &str) -> Result SecretResolver { + let (_, resolver) = SecretResolver::from_provider_env_for_revision( + HashMap::from([("TOKEN".to_string(), "secret-value".to_string())]), + HashMap::new(), + 1, + ); + resolver + .unwrap() + .with_placements(HashMap::from([("TOKEN".to_string(), placement)])) + } + + #[test] + fn missing_placement_map_defaults_request_rewriting_to_authorization() { + let (_, resolver) = SecretResolver::from_provider_env(HashMap::from([( + "TOKEN".into(), + "synthetic".into(), + )])); + let resolver = resolver.unwrap(); + for token in [ + "openshell:resolve:env:TOKEN", + "vendor-OPENSHELL-RESOLVE-ENV-TOKEN", + ] { + for scheme in ["Bearer", "token"] { + assert_eq!( + resolver + .rewrite_header_value_for_name( + "aUtHoRiZaTiOn", + &format!("{scheme} {token}") + ) + .unwrap(), + Some(format!("{scheme} synthetic")) + ); + } + let basic = base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Basic {basic}")) + .unwrap() + .is_some() + ); + for name in ["X-Reflect", "X-Api-Key"] { + assert!( + resolver + .rewrite_header_value_for_name(name, token) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + resolver + .rewrite_header_value_for_name(name, &format!("Basic {basic}")) + .unwrap_err() + .is_placement_mismatch() + ); + } + for target in [ + format!("/search?token={token}"), + format!( + "/search?token={}&token={}", + fully_percent_encoded(token), + fully_percent_encoded(token) + ), + format!("/api/{token}"), + ] { + assert!( + rewrite_target_for_eval(&target, &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + } + } + // These methods are called only after explicit endpoint-policy opt-in. + assert_eq!( + resolver + .rewrite_text_placeholders( + &mut "openshell:resolve:env:TOKEN".into(), + "request_body" + ) + .unwrap(), + 1 + ); + assert_eq!( + resolver + .rewrite_websocket_text_placeholders(&mut "openshell:resolve:env:TOKEN".into()) + .unwrap(), + 1 + ); + assert_eq!( + resolver + .resolve_current_env_key_checked("TOKEN", "sigv4") + .unwrap(), + Some("synthetic") + ); + } + + #[test] + fn static_header_placement_cannot_be_overridden_by_request_location() { + let resolver = placement_resolver(CredentialPlacement::Header("Authorization".into())); + let token = "openshell:resolve:env:v1_TOKEN"; + assert_eq!( + resolver + .rewrite_header_value_for_name("aUtHoRiZaTiOn", &format!("token {token}")) + .unwrap(), + Some("token secret-value".into()) + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", token) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/search?q={token}"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/bot{token}/events"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert_eq!( + resolver + .rewrite_text_placeholders(&mut token.to_string(), "body") + .unwrap(), + 1 + ); + assert_eq!( + resolver + .rewrite_websocket_text_placeholders(&mut token.to_string()) + .unwrap(), + 1 + ); + assert!(resolver.resolve_placeholder(token).is_none()); + let basic = base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")); + assert!( + resolver + .rewrite_header_value_for_name("Authorization", &format!("Basic {basic}")) + .unwrap() + .is_some() + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Reflect", &format!("Basic {basic}")) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[test] + fn query_placement_checks_each_decoded_parameter_name() { + let resolver = placement_resolver(CredentialPlacement::QueryParam("api_key".into())); + let token = "openshell:resolve:env:v1_TOKEN"; + let valid = format!("/search?%61pi_key={token}"); + assert_eq!( + rewrite_target_for_eval(&valid, &resolver).unwrap().resolved, + "/search?%61pi_key=secret-value" + ); + for target in [ + format!("/search?other={token}"), + format!("/search?api_key={token}&other={token}"), + "/search?other=vendor-OPENSHELL-RESOLVE-ENV-v1_TOKEN".to_string(), + ] { + assert!( + rewrite_target_for_eval(&target, &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + } + assert!( + rewrite_target_for_eval( + &format!("/search?api_key={token}&api_key={token}"), + &resolver + ) + .is_ok() + ); + } + + #[test] + fn path_placement_checks_exact_slot_and_literal_template() { + let resolver = placement_resolver(CredentialPlacement::PathTemplate( + "/bot{credential}/events".into(), + )); + let token = "openshell:resolve:env:v1_TOKEN"; + assert_eq!( + rewrite_target_for_eval(&format!("/bot{token}/events"), &resolver) + .unwrap() + .resolved, + "/botsecret-value/events" + ); + assert!( + rewrite_target_for_eval(&format!("/bot{token}/echo"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval(&format!("/echo/{token}/events"), &resolver) + .unwrap_err() + .is_placement_mismatch() + ); + assert!( + rewrite_target_for_eval( + &format!("/bot{}/events", fully_percent_encoded(token)), + &resolver + ) + .is_ok() + ); + let alias_resolver = placement_resolver(CredentialPlacement::PathTemplate( + "/{credential}/events".into(), + )); + assert!( + rewrite_target_for_eval( + "/vendor-OPENSHELL-RESOLVE-ENV-v1_TOKEN/events", + &alias_resolver + ) + .is_ok() + ); + } + fn fully_percent_encoded(marker: &str) -> String { const HEX: &[u8; 16] = b"0123456789ABCDEF"; let mut encoded = String::with_capacity(marker.len() * 3); @@ -1553,7 +1942,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "CUSTOM_TOKEN".into(), + CredentialPlacement::Header("x-api-key".into()), + )])); assert_eq!( rewrite_header_line("x-api-key: openshell:resolve:env:CUSTOM_TOKEN", &resolver), @@ -1632,7 +2024,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "CHAT_APP_TOKEN".into(), + CredentialPlacement::Header("x-app-token".into()), + )])); assert_eq!( rewrite_header_line( @@ -1711,6 +2106,12 @@ mod tests { x-api-key: {token_value}\r\n\ Content-Length: 0\r\n\r\n" ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "CUSTOM_SERVICE_TOKEN".into(), + CredentialPlacement::Header("x-api-key".into()), + )])) + }); let result = rewrite_http_header_block(raw.as_bytes(), resolver.as_ref()).expect("should succeed"); @@ -1815,6 +2216,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "YOUTUBE_API_KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])) + }); let placeholder = child_env.get("YOUTUBE_API_KEY").unwrap(); let raw = format!( @@ -1843,6 +2250,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "API_KEY".into(), + CredentialPlacement::QueryParam("token".into()), + )])) + }); let placeholder = child_env.get("API_KEY").unwrap(); let raw = format!("GET /api?token={placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -1863,6 +2276,12 @@ mod tests { .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([( + "KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])) + }); let placeholder = child_env.get("KEY").unwrap(); let raw = format!("GET /api?key={placeholder}&format=json HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2004,7 +2423,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let raw = format!("GET /api/{placeholder}/data HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2032,7 +2454,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TELEGRAM_TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/sendMessage".into()), + )])); let placeholder = child_env.get("TELEGRAM_TOKEN").unwrap(); let raw = format!( @@ -2067,6 +2492,16 @@ mod tests { let resolver = resolver.expect("resolver"); let org_ph = child_env.get("ORG_ID").unwrap(); let key_ph = child_env.get("API_KEY").unwrap(); + let resolver = resolver.with_placements(HashMap::from([ + ( + "ORG_ID".into(), + CredentialPlacement::PathTemplate(format!("/orgs/{{credential}}/keys/{key_ph}")), + ), + ( + "API_KEY".into(), + CredentialPlacement::PathTemplate(format!("/orgs/{org_ph}/keys/{{credential}}")), + ), + ])); let raw = format!("GET /orgs/{org_ph}/keys/{key_ph} HTTP/1.1\r\nHost: x\r\n\r\n"); let result = @@ -2100,7 +2535,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/method".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let raw = format!("GET /bot{placeholder}/method?format=json HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2121,7 +2559,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder}/data HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2139,7 +2580,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2157,7 +2601,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2175,7 +2622,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "BAD".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + )])); let placeholder = child_env.get("BAD").unwrap(); let raw = format!("GET /api/{placeholder} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2196,7 +2646,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/api/{credential}/data".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); // Space in the credential should trigger path validation rejection @@ -2283,7 +2736,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "API_TOKEN".into(), + CredentialPlacement::QueryParam("token".into()), + )])); let encoded = "openshell%3Aresolve%3Aenv%3AAPI_TOKEN"; let raw = format!("GET /api?token={encoded} HTTP/1.1\r\nHost: x\r\n\r\n"); @@ -2305,16 +2761,31 @@ mod tests { [ ("KEY1".to_string(), "secret1".to_string()), ("KEY2".to_string(), "secret2".to_string()), + ("KEY3".to_string(), "secret3".to_string()), ] .into_iter() .collect(), ); + let resolver = resolver.map(|resolver| { + resolver.with_placements(HashMap::from([ + ( + "KEY1".into(), + CredentialPlacement::PathTemplate("/api/{credential}".into()), + ), + ( + "KEY2".into(), + CredentialPlacement::QueryParam("token".into()), + ), + ("KEY3".into(), CredentialPlacement::Header("x-auth".into())), + ])) + }); let ph1 = child_env.get("KEY1").unwrap(); let ph2 = child_env.get("KEY2").unwrap(); + let ph3 = child_env.get("KEY3").unwrap(); let raw = format!( "GET /api/{ph1}?token={ph2} HTTP/1.1\r\n\ - x-auth: {ph1}\r\n\r\n" + x-auth: {ph3}\r\n\r\n" ); let result = rewrite_http_header_block(raw.as_bytes(), resolver.as_ref()).expect("should succeed"); @@ -2498,7 +2969,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/sendMessage".into()), + )])); let placeholder = child_env.get("TOKEN").unwrap(); let result = rewrite_target_for_eval(&format!("/bot{placeholder}/sendMessage"), &resolver) @@ -2516,7 +2990,10 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([( + "KEY".into(), + CredentialPlacement::QueryParam("key".into()), + )])); let placeholder = child_env.get("KEY").unwrap(); let result = @@ -2614,7 +3091,13 @@ mod tests { .into_iter() .collect(), ); - let resolver = resolver.expect("resolver"); + let resolver = resolver.expect("resolver").with_placements(HashMap::from([ + ( + "TOKEN".into(), + CredentialPlacement::PathTemplate("/bot{credential}/method".into()), + ), + ("KEY".into(), CredentialPlacement::QueryParam("key".into())), + ])); let tok_ph = child_env.get("TOKEN").unwrap(); let key_ph = child_env.get("KEY").unwrap(); diff --git a/crates/openshell-server/README.md b/crates/openshell-server/README.md index a1053f582c..3c52ebb127 100644 --- a/crates/openshell-server/README.md +++ b/crates/openshell-server/README.md @@ -167,6 +167,25 @@ A provisioning timeout can be repaired by Stop after its original driver work settles and cleanup completes. Stop exposes Stopped without discarding the timeout diagnosis or storage. This lifecycle behavior also applies when updates are disabled. +## Static credential delivery + +Existing polling and configuration-push environments keep their original wire +shape, delivery behavior, identities, and stable-handle derivation. The gateway +adds placement-only `ProviderProfileCredential` entries to the existing runtime +credential map under `static-env:`, for actually delivered static keys. +Entries are matched to their owning provider binding and contain no token grant, +refresh configuration, or credential values. The supervisor infers enforcement +from the auth metadata; the gateway does not enforce placement. + +Older supervisors ignore entries without token grants. An older gateway omits +these entries, so newer supervisors permit static substitution only in +`Authorization` or explicitly enabled body/WebSocket rewrite contexts. Custom +placements require a supporting gateway; upgrade it before the supervisor. +Explicit authored placement is forwarded even when invalid so the supervisor +can reject it rather than silently apply the default. Metadata travels in the +same captured environment snapshot; no separate service, capability gate, or +additional lookup is needed. + ## Operator credential export `GetProviderCredentials` exports selected profile-declared runtime environment diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index f5c7c94480..cc83789417 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -4061,6 +4061,18 @@ pub async fn build_provider_environment_snapshot_from_inputs( .clear(); } + // Add request metadata only after token-grant owner composition and all + // static value filtering, so it describes this exact atomic snapshot. + let static_request_metadata = super::provider::static_credential_request_metadata( + &sandbox_id, + provider_profile_catalog, + provider_records, + &provider_environment, + )?; + provider_environment + .dynamic_credentials + .extend(static_request_metadata); + info!( sandbox_id = %sandbox_id, provider_count = provider_names.len(), @@ -12376,13 +12388,27 @@ mod tests { environment.provider_env_revision, second.provider_env_revision ); - assert_eq!(environment.dynamic_credentials.len(), 2); - assert!(environment.dynamic_credentials.values().all(|credential| { - credential - .token_grant - .as_ref() - .is_some_and(|grant| grant.token_endpoint.contains("revision-b")) - })); + let grants = environment + .dynamic_credentials + .values() + .filter_map(|credential| credential.token_grant.as_ref()) + .collect::>(); + assert_eq!(grants.len(), 2); + assert!( + grants + .iter() + .all(|grant| grant.token_endpoint.contains("revision-b")) + ); + assert!( + environment + .dynamic_credentials + .iter() + .filter(|(key, _)| key.starts_with( + openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX + )) + .all(|(_, credential)| credential.token_grant.is_none() + && credential.token_grant_owners.is_empty()) + ); handle_submit_policy_analysis( &state, diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 0f09d5dc2f..6ef3c3b15d 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -1566,6 +1566,78 @@ pub(super) async fn resolve_provider_environment_from_records_with_policy_bindin }) } +/// Copy authored request metadata for static values that survived snapshot +/// filtering. The reserved namespace keeps it separate from endpoint token grants; +/// older supervisors ignore entries without `token_grant` before parsing selectors. +pub(super) fn static_credential_request_metadata( + sandbox_id: &str, + catalog: &EffectiveProviderProfileCatalog, + records: &[ProviderEnvironmentRecord], + environment: &ProviderEnvironment, +) -> Result, Status> { + use openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX; + let mut metadata = HashMap::new(); + for record in records { + let provider = &record.provider; + let Some(profile) = get_provider_type_profile_for_scope( + catalog, + &provider.r#type, + &provider.profile_workspace, + ) else { + continue; + }; + let refresh_epochs = refresh_authorization_epochs_by_key(record)?; + for credential in &profile.credentials { + // Keep malformed explicit metadata distinguishable from a wholly + // absent declaration. The supervisor validates authored fields and + // fails closed; absence selects its default request placement. + if credential.auth_style.is_empty() + && credential.header_name.is_empty() + && credential.query_param.is_empty() + && credential.path_template.is_empty() + { + continue; + } + for key in credential.accepted_stored_keys() { + if !environment.environment.contains_key(key) + || !environment.static_credential_keys.contains(key) + { + continue; + } + let Some(baseline) = environment.static_credential_bindings.get(key) else { + continue; + }; + // A later optional declaration with the same environment key + // must not override metadata belonging to the actual owner. + let expected = static_credential_binding( + Some(sandbox_id), + record, + key, + &baseline.endpoints, + refresh_epochs.get(key).map(String::as_str), + credential.stable_placeholder, + ); + if expected != *baseline { + continue; + } + metadata.insert( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}{key}"), + ProviderProfileCredential { + name: credential.name.clone(), + env_vars: vec![key.to_string()], + auth_style: credential.auth_style.clone(), + header_name: credential.header_name.clone(), + query_param: credential.query_param.clone(), + path_template: credential.path_template.clone(), + ..Default::default() + }, + ); + } + } + } + Ok(metadata) +} + fn refresh_authorization_epochs_by_key( record: &ProviderEnvironmentRecord, ) -> Result, Status> { @@ -12223,6 +12295,142 @@ mod tests { ); } + #[tokio::test] + async fn static_request_metadata_is_minimal_owner_bound_and_tracks_delivered_values() { + use openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX; + let store = test_store().await; + let mut owned = custom_profile("metadata-owner"); + owned.credentials = vec![static_credential("token", "TOKEN", true)]; + owned.endpoints = vec![NetworkEndpoint { + host: "api.example.com".into(), + port: 443, + protocol: "rest".into(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, + ..Default::default() + }]; + let mut optional = owned.clone(); + optional.id = "metadata-optional".into(); + optional.credentials[0].required = false; + optional.credentials[0].header_name = "x-wrong-owner".into(); + let sources = ProviderProfileSources::from_test_profiles(vec![owned.clone(), optional]); + let catalog = sources.snapshot_catalog(&store, "default").await.unwrap(); + let record = |name: &str, id: &str, profile: &str| ProviderEnvironmentRecord { + name: name.into(), + object_id: id.into(), + resource_version: 1, + provider: Provider { + r#type: profile.into(), + profile_workspace: "default".into(), + ..Default::default() + }, + refresh_states: Vec::new(), + }; + let mut records = [ + record("owner", "owner-id", "metadata-owner"), + record("optional", "optional-id", "metadata-optional"), + ]; + records[0] + .provider + .credentials + .insert("TOKEN".into(), "synthetic-sensitive-token".into()); + let endpoints = vec![StaticCredentialEndpointBinding { + host: "api.example.com".into(), + port: 443, + path: "/**".into(), + }]; + let baseline = static_credential_binding( + Some("sandbox"), + &records[0], + "TOKEN", + &endpoints, + None, + false, + ); + let mut environment = ProviderEnvironment { + environment: HashMap::from([("TOKEN".into(), "synthetic-sensitive-token".into())]), + static_credential_keys: HashSet::from(["TOKEN".into()]), + static_credential_bindings: HashMap::from([("TOKEN".into(), baseline.clone())]), + ..Default::default() + }; + let metadata = + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap(); + let key = format!("{STATIC_CREDENTIAL_METADATA_PREFIX}TOKEN"); + assert_eq!(metadata.len(), 1); + assert_eq!( + metadata[&key], + ProviderProfileCredential { + name: "token".into(), + env_vars: vec!["TOKEN".into()], + auth_style: "bearer".into(), + header_name: "authorization".into(), + ..Default::default() + } + ); + assert_eq!( + environment.static_credential_bindings["TOKEN"], baseline, + "legacy handles and identities must not change" + ); + let encoded = metadata[&key].encode_to_vec(); + assert!( + !encoded + .windows(b"synthetic-sensitive-token".len()) + .any(|window| window == b"synthetic-sensitive-token") + ); + // Preserve malformed authored fields for the supervisor to reject; + // silently dropping them would incorrectly select its default location. + for (style, header, query, path, explicit, valid) in [ + ("", "", "", "", false, true), + (" ", "", "", "", true, false), + ("", "x-api-key", "", "", true, false), + ("header", "Bad Header", "", "", true, false), + ("bearer", "authorization", "api_key", "", true, true), + ( + "bearer", + "authorization", + "api_key", + "/v1/{credential}/resources", + true, + true, + ), + ("query", "", "api?key", "", true, false), + ("basic", "x-custom", "", "", true, true), + ("basic", "Authorization", "", "", true, true), + ] { + let mut declared = owned.clone(); + declared.credentials[0].auth_style = style.into(); + declared.credentials[0].header_name = header.into(); + declared.credentials[0].query_param = query.into(); + declared.credentials[0].path_template = path.into(); + let sources = ProviderProfileSources::from_test_profiles(vec![declared]); + let catalog = sources.snapshot_catalog(&store, "default").await.unwrap(); + let metadata = + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap(); + assert_eq!( + metadata.contains_key(&key), + explicit, + "style {style:?}, header {header:?}" + ); + let installed = openshell_core::provider_credentials::ProviderCredentialState::from_bound_environment( + 1, environment.environment.clone(), HashMap::new(), metadata, + environment.static_credential_bindings.clone(), vec![], + ); + assert_eq!( + installed.is_ok(), + valid, + "style {style:?}, header {header:?}, query {query:?}" + ); + } + environment.environment.remove("TOKEN"); + assert!( + static_credential_request_metadata("sandbox", &catalog, &records, &environment) + .unwrap() + .is_empty(), + "expired or withheld material must not leave request metadata behind" + ); + } + #[test] fn external_stable_placeholder_binding_preserves_only_value_changes() { let mut record = ProviderEnvironmentRecord { @@ -12548,6 +12756,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-1") ); @@ -12608,6 +12819,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-2") ); @@ -12625,6 +12839,9 @@ mod tests { reconstructed .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), Some("token-2") ); @@ -12676,6 +12893,9 @@ mod tests { credential_state .resolver_for_endpoint("api.example.com", 443, "/v1/messages") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "authorization".into() + )) .resolve_placeholder(&original_placeholder), None ); diff --git a/crates/openshell-supervisor-network/README.md b/crates/openshell-supervisor-network/README.md index cf842d5041..38a7fb6add 100644 --- a/crates/openshell-supervisor-network/README.md +++ b/crates/openshell-supervisor-network/README.md @@ -41,3 +41,13 @@ at debug, and emits no OCSF denial event: a standard OpenTelemetry SDK retries for the life of the agent, and one event per retry would flood the OCSF log. The supervisor installs its OTLP relay receiver as the handler while the agent trace relay is active. + +## Static credential placement + +Substitution checks the declared request location before writing upstream. +Credentials without placement metadata default to the Authorization header. HTTP placement violations return a safe 403 response; +WebSocket violations close the connection with policy denial. Both paths emit a +domain event and a detection finding without credential values or placeholders. +Explicit body and WebSocket +rewrite flags authorize those locations for ordinary static credentials. SigV4 +signing retains the endpoint-scoped resolver's existing trusted access. diff --git a/crates/openshell-supervisor-network/src/l7/mod.rs b/crates/openshell-supervisor-network/src/l7/mod.rs index 3f26114998..37849b9c0c 100644 --- a/crates/openshell-supervisor-network/src/l7/mod.rs +++ b/crates/openshell-supervisor-network/src/l7/mod.rs @@ -66,6 +66,37 @@ pub(crate) fn build_credential_endpoint_mismatch_finding( .build() } +pub(crate) fn build_credential_placement_mismatch_finding( + policy_name: &str, + host: &str, + protocol: Option<&str>, +) -> openshell_ocsf::OcsfEvent { + use openshell_ocsf::{ + ActionId, ActivityId, DetectionFindingBuilder, DispositionId, FindingInfo, SeverityId, + }; + let mut evidence = vec![ + ("policy", policy_name), + ("host", host), + ("disposition", "denied"), + ]; + if let Some(protocol) = protocol { + evidence.push(("protocol", protocol)); + } + DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Open) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .is_alert(true) + .finding_info(FindingInfo::new( + "openshell.provider_credential.placement_mismatch", + "Provider credential used at an unauthorized request location", + )) + .evidence_pairs(&evidence) + .message("Provider credential placement mismatch; request denied") + .build() +} + /// TLS handling mode for proxy connections. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TlsMode { diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index e2d9530a56..b4acb5e52f 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -593,12 +593,15 @@ where W: AsyncWrite + Unpin, { let endpoint_mismatch = error.is_endpoint_mismatch(); - let status = if endpoint_mismatch { + let placement_mismatch = error.is_placement_mismatch(); + let status = if endpoint_mismatch || placement_mismatch { "403 Forbidden" } else { "500 Internal Server Error" }; - let body = if endpoint_mismatch { + let body = if placement_mismatch { + r#"{"error":"credential_placement_mismatch","message":"Credential is not authorized for this request location"}"# + } else if endpoint_mismatch { r#"{"error":"credential_endpoint_mismatch","message":"Credential is not authorized for this request endpoint"}"# } else { r#"{"error":"credential_unavailable","message":"Credential placeholder could not be resolved"}"# @@ -613,11 +616,34 @@ where .into_diagnostic()?; client.flush().await.into_diagnostic()?; - ocsf_emit!(build_credential_resolution_event( - ctx, - method, - endpoint_mismatch - )); + if placement_mismatch { + ocsf_emit!( + HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::for_http_method(method)) + .http_request(method_only_request(method)) + .http_response(openshell_ocsf::HttpResponse { code: 403 }) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) + .firewall_rule(&ctx.policy_name, "credential-binding") + .message("Credential use denied: unauthorized request location") + .status_detail("credential_placement_mismatch") + .build() + ); + ocsf_emit!(crate::l7::build_credential_placement_mismatch_finding( + &ctx.policy_name, + &ctx.host, + None, + )); + } else { + ocsf_emit!(build_credential_resolution_event( + ctx, + method, + endpoint_mismatch + )); + } if endpoint_mismatch { ocsf_emit!(build_credential_endpoint_mismatch_finding(ctx)); @@ -1884,9 +1910,13 @@ where assembly_budget, resolver, generation_guard: options.generation_guard, - provider_credentials: options - .ctx - .and_then(|ctx| ctx.provider_credentials.as_ref()), + provider_credentials: if options.websocket.credential_rewrite { + options + .ctx + .and_then(|ctx| ctx.provider_credentials.as_ref()) + } else { + None + }, target: &options.target, inspector, compression, @@ -4109,6 +4139,9 @@ mod tests { scoped .secret_resolver .expect("endpoint resolver") + .with_request_location(secrets::RequestCredentialLocation::Header( + "Authorization".into() + )) .resolve_placeholder("openshell:resolve:env:v42_API_TOKEN"), Some("secret") ); @@ -4292,6 +4325,570 @@ mod tests { assert_eq!(finding["disposition"], "Blocked"); } + fn placement_test_state() -> ProviderCredentialState { + placement_test_state_with_metadata(true) + } + + fn placement_test_state_with_metadata(with_metadata: bool) -> ProviderCredentialState { + ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([("API_TOKEN".into(), "synthetic-private-token".into())]), + TestHashMap::new(), + if with_metadata { + crate::test_support::header_placement_metadata(&["API_TOKEN"]) + } else { + TestHashMap::new() + }, + TestHashMap::from([("API_TOKEN".into(), endpoint_binding("provider:API_TOKEN"))]), + Vec::new(), + ) + .expect("bound placement fixture") + } + + #[tokio::test] + async fn mixed_declared_and_fallback_credentials_preserve_opt_in_body_rewriting() { + let fallback_binding = endpoint_binding("provider:FALLBACK_TOKEN"); + let state = ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([ + ("STRICT_TOKEN".into(), "strict-private-token".into()), + ("FALLBACK_TOKEN".into(), "fallback-private-token".into()), + ]), + TestHashMap::new(), + crate::test_support::header_placement_metadata(&["STRICT_TOKEN"]), + TestHashMap::from([ + ( + "STRICT_TOKEN".into(), + endpoint_binding("provider:STRICT_TOKEN"), + ), + ("FALLBACK_TOKEN".into(), fallback_binding), + ]), + Vec::new(), + ) + .expect("mixed placement fixture"); + let snapshot = state.snapshot(); + let strict = &snapshot.child_env["STRICT_TOKEN"]; + let fallback = &snapshot.child_env["FALLBACK_TOKEN"]; + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let body = format!("fallback={fallback}&strict={strict}"); + let target = "/allowed/search?probe=literal"; + let raw = format!( + "POST {target} HTTP/1.1\r\nHost: allowed.example.test\r\nAuthorization: token {strict}\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "POST", + "/allowed/search", + target, + raw.into_bytes(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + let headers = String::from_utf8(bytes).unwrap(); + let length = headers + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("Content-Length") + .then(|| value.trim().parse::().unwrap()) + }) + .unwrap(); + let mut body = vec![0; length]; + service.read_exact(&mut body).await.unwrap(); + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + (headers, String::from_utf8(body).unwrap()) + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: true, + ..Default::default() + }, + ) + .await + .unwrap(); + let (headers, body) = service_task.await.unwrap(); + assert!(headers.contains("/allowed/search?probe=literal")); + assert!(headers.contains("Authorization: token strict-private-token")); + assert_eq!( + body, + "fallback=fallback-private-token&strict=strict-private-token" + ); + assert!( + resolver + .rewrite_header_value_for_name("X-Legacy", strict) + .unwrap_err() + .is_placement_mismatch() + ); + } + + #[tokio::test] + async fn metadata_credentials_do_not_enable_disabled_body_rewriting() { + let state = placement_test_state(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let body = format!("token={token}"); + let raw = format!( + "POST /allowed/search HTTP/1.1\r\nHost: allowed.example.test\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "POST", + "/allowed/search", + "/allowed/search", + raw.into_bytes(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let error = crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: false, + deny_uninspected_credentials: true, + ..Default::default() + }, + ) + .await + .expect_err("placement metadata must not enable body rewriting"); + assert!( + error + .downcast_ref::() + .is_some(), + "{error}" + ); + drop(upstream); + let mut sent = String::new(); + service.read_to_string(&mut sent).await.unwrap(); + assert!(!sent.contains("synthetic-private-token")); + } + + #[tokio::test] + async fn metadata_credentials_do_not_enable_disabled_websocket_rewriting() { + let state = placement_test_state(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let ctx = L7EvalContext { + host: "allowed.example.test".into(), + port: 443, + provider_credentials: Some(state.clone()), + ..Default::default() + }; + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + handle_upgrade( + &mut client, + &mut upstream, + Vec::new(), + "allowed.example.test", + 443, + UpgradeRelayOptions { + websocket_request: true, + websocket: WebSocketUpgradeBehavior { + credential_rewrite: false, + deny_uninspected_credentials: true, + ..Default::default() + }, + secret_resolver: state.resolver(), + ctx: Some(&ctx), + target: "/allowed/socket".into(), + ..Default::default() + }, + ) + .await + }); + app.write_all(&masked_text_frame(token.as_bytes())) + .await + .unwrap(); + let error = tokio::time::timeout(std::time::Duration::from_secs(1), relay) + .await + .unwrap() + .unwrap() + .expect_err("disabled rewriting must deny credential frames"); + assert!(error.to_string().contains("rewrite is disabled"), "{error}"); + let mut sent = Vec::new(); + service.read_to_end(&mut sent).await.unwrap(); + assert!(sent.is_empty()); + } + + #[tokio::test] + async fn header_credential_query_reflection_is_denied_before_upstream_write() { + for with_metadata in [true, false] { + let state = placement_test_state_with_metadata(with_metadata); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + // A pagination service copies extra query parameters into its Link header. + // No request bytes may reach it when the parameter contains a header credential. + for (target, extra_headers, body, rewrite_body) in [ + ( + format!("/allowed/{token}/search"), + String::new(), + String::new(), + false, + ), + ( + format!("/allowed/search?q=rust&per_page=1&probe={token}"), + String::new(), + String::new(), + false, + ), + ( + "/allowed/search".into(), + format!("X-Reflect: {token}\r\n"), + String::new(), + false, + ), + ] { + let raw = format!( + "GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\n{extra_headers}Content-Type: application/x-www-form-urlencoded\r\nContent-Length: {}\r\n\r\n{body}", + body.len() + ); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/search", + &target, + raw.into_bytes(), + ) + .unwrap(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let ctx = L7EvalContext { + host: "allowed.example.test".into(), + port: 443, + ..Default::default() + }; + let outcome = relay_http_request_with_credential_rejection( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + request_body_credential_rewrite: rewrite_body, + ..Default::default() + }, + &ctx, + None, + ) + .await + .unwrap(); + assert!(outcome.is_none()); + drop(client); + drop(upstream); + let mut response = String::new(); + app.read_to_string(&mut response).await.unwrap(); + let mut sent = Vec::new(); + service.read_to_end(&mut sent).await.unwrap(); + assert!( + sent.is_empty(), + "placement denial must precede all upstream writes" + ); + assert!(response.contains("403 Forbidden"), "{response}"); + assert!( + response.contains("credential_placement_mismatch"), + "{response}" + ); + assert!(!response.contains("synthetic-private-token")); + assert!(!response.contains(&token)); + } + let finding = crate::l7::build_credential_placement_mismatch_finding( + "test", + "allowed.example.test", + None, + ) + .to_json() + .unwrap(); + assert_eq!( + finding["finding_info"]["uid"], + "openshell.provider_credential.placement_mismatch" + ); + } + } + + #[tokio::test] + async fn permitted_auth_formats_preserve_pagination_without_disclosing_credentials() { + use base64::Engine as _; + for with_metadata in [true, false] { + let state = placement_test_state_with_metadata(with_metadata); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/search") + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + for (authorization, expected) in [ + ( + format!("Bearer {token}"), + "Bearer synthetic-private-token".to_owned(), + ), + ( + format!("token {token}"), + "token synthetic-private-token".to_owned(), + ), + ( + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD.encode(format!("user:{token}")) + ), + format!( + "Basic {}", + base64::engine::general_purpose::STANDARD + .encode("user:synthetic-private-token") + ), + ), + ] { + let target = "/allowed/search?q=rust&per_page=1"; + let raw = format!( + "GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\nAuthorization: {authorization}\r\n\r\n" + ); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/search", + target, + raw.into_bytes(), + ) + .unwrap(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + let received = String::from_utf8(bytes).unwrap(); + let target = received + .lines() + .next() + .unwrap() + .split_whitespace() + .nth(1) + .unwrap(); + let response = format!( + "HTTP/1.1 200 OK\r\nLink: ; rel=\"next\"\r\nContent-Length: 0\r\n\r\n" + ); + service.write_all(response.as_bytes()).await.unwrap(); + received + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + ..Default::default() + }, + ) + .await + .unwrap(); + drop(client); + let mut response = String::new(); + app.read_to_string(&mut response).await.unwrap(); + let received = service_task.await.unwrap(); + assert!( + received.contains(&format!("Authorization: {expected}")), + "{received}" + ); + assert!(response.contains("per_page=1&page=2"), "{response}"); + assert!(!response.contains("synthetic-private-token")); + } + } + } + + #[tokio::test] + async fn declared_non_authorization_locations_reach_the_upstream() { + for location in ["header", "query", "path"] { + let mut metadata = crate::test_support::header_placement_metadata(&["API_TOKEN"]); + let declaration = metadata.values_mut().next().unwrap(); + declaration.auth_style = location.into(); + declaration.header_name = if location == "header" { + "x-api-key" + } else { + "" + } + .into(); + declaration.query_param = if location == "query" { "api_key" } else { "" }.into(); + declaration.path_template = if location == "path" { + "/allowed/{credential}/events" + } else { + "" + } + .into(); + let state = ProviderCredentialState::from_bound_environment( + 1, + TestHashMap::from([("API_TOKEN".into(), "synthetic-private-token".into())]), + TestHashMap::new(), + metadata, + TestHashMap::from([("API_TOKEN".into(), endpoint_binding("provider:API_TOKEN"))]), + Vec::new(), + ) + .unwrap(); + let token = state.snapshot().child_env["API_TOKEN"].clone(); + let (target, header, expected) = match location { + "header" => ( + "/allowed/search".into(), + format!("x-api-key: {token}\r\n"), + "x-api-key: synthetic-private-token", + ), + "query" => ( + format!("/allowed/search?api_key={token}"), + String::new(), + "/allowed/search?api_key=synthetic-private-token", + ), + _ => ( + format!("/allowed/{token}/events"), + String::new(), + "/allowed/synthetic-private-token/events", + ), + }; + let path = target.split('?').next().unwrap(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, path) + .unwrap(); + let raw = + format!("GET {target} HTTP/1.1\r\nHost: allowed.example.test\r\n{header}\r\n"); + let request = + crate::l7::rest::request_from_buffered_http("GET", path, &target, raw.into_bytes()) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + String::from_utf8(bytes).unwrap() + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + ..Default::default() + }, + ) + .await + .unwrap(); + let sent = service_task.await.unwrap(); + assert!(sent.contains(expected), "{location}: {sent}"); + assert!(!sent.contains(&token)); + } + } + + #[tokio::test] + async fn trusted_sigv4_signing_preserves_metadata_free_credentials() { + let values = TestHashMap::from([ + ("AWS_ACCESS_KEY_ID".into(), "synthetic-access-key".into()), + ( + "AWS_SECRET_ACCESS_KEY".into(), + "synthetic-signing-secret".into(), + ), + ("AWS_SESSION_TOKEN".into(), "synthetic-session-token".into()), + ]); + let bindings = values + .keys() + .map(|key: &String| { + let binding = endpoint_binding(&format!("aws:{key}")); + (key.clone(), binding) + }) + .collect(); + let state = ProviderCredentialState::from_bound_environment( + 1, + values, + TestHashMap::new(), + TestHashMap::new(), + bindings, + Vec::new(), + ) + .unwrap(); + let resolver = state + .resolver_for_endpoint("allowed.example.test", 443, "/allowed/api") + .unwrap(); + let request = crate::l7::rest::request_from_buffered_http( + "GET", + "/allowed/api", + "/allowed/api", + b"GET /allowed/api HTTP/1.1\r\nHost: allowed.example.test\r\n\r\n".to_vec(), + ) + .unwrap(); + let (mut _app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut service) = tokio::io::duplex(8192); + let service_task = tokio::spawn(async move { + let mut bytes = Vec::new(); + loop { + let mut byte = [0]; + service.read_exact(&mut byte).await.unwrap(); + bytes.push(byte[0]); + if bytes.ends_with(b"\r\n\r\n") { + break; + } + } + service + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + String::from_utf8(bytes).unwrap() + }); + crate::l7::rest::relay_http_request_with_options_guarded( + &request, + &mut client, + &mut upstream, + crate::l7::rest::RelayRequestOptions { + resolver: Some(&resolver), + credential_signing: crate::l7::CredentialSigning::SigV4NoBody, + signing_service: "execute-api", + signing_region: "us-west-2", + host: "allowed.example.test", + port: 443, + ..Default::default() + }, + ) + .await + .unwrap(); + let sent = service_task.await.unwrap(); + assert!(sent.contains("AWS4-HMAC-SHA256"), "{sent}"); + assert!(sent.contains("synthetic-access-key")); + assert!(sent.contains("synthetic-session-token")); + assert!(!sent.contains("synthetic-signing-secret")); + } + #[tokio::test] async fn body_only_endpoint_mismatch_returns_typed_403() { let (_state, resolver) = endpoint_mismatch_resolver(TestHashMap::from([( diff --git a/crates/openshell-supervisor-network/src/l7/rest.rs b/crates/openshell-supervisor-network/src/l7/rest.rs index 7b85942666..7a6a0d6236 100644 --- a/crates/openshell-supervisor-network/src/l7/rest.rs +++ b/crates/openshell-supervisor-network/src/l7/rest.rs @@ -9670,6 +9670,70 @@ mod tests { // receives. // ========================================================================= + fn placed_test_resolver( + environment: HashMap, + host: &str, + declarations: &[(&str, &str, &str)], + ) -> (HashMap, Option) { + use openshell_core::proto::{ + ProviderProfileCredential, StaticCredentialBinding, StaticCredentialEndpointBinding, + }; + use openshell_core::provider_credentials::{ + ProviderCredentialState, STATIC_CREDENTIAL_METADATA_PREFIX, + }; + let metadata = declarations + .iter() + .map(|(key, style, selector)| { + let mut credential = ProviderProfileCredential { + env_vars: vec![(*key).into()], + auth_style: (*style).into(), + ..Default::default() + }; + match *style { + "header" => credential.header_name = (*selector).into(), + "query" => credential.query_param = (*selector).into(), + "path" => credential.path_template = (*selector).into(), + _ => panic!("fixture needs an explicit placement"), + } + ( + format!("{STATIC_CREDENTIAL_METADATA_PREFIX}{key}"), + credential, + ) + }) + .collect(); + let bindings = environment + .keys() + .map(|key| { + ( + key.clone(), + StaticCredentialBinding { + credential_identity: format!("fixture:{key}"), + workload_credential_handle: String::new(), + endpoints: vec![StaticCredentialEndpointBinding { + host: host.into(), + port: 443, + path: "/**".into(), + }], + }, + ) + }) + .collect(); + let state = ProviderCredentialState::from_bound_environment( + 1, + environment, + HashMap::new(), + metadata, + bindings, + Vec::new(), + ) + .unwrap(); + let child_env = state.snapshot().child_env.clone(); + let resolver = state + .resolver_for_endpoint(host, 443, "/") + .map(|resolver| resolver.as_ref().clone()); + (child_env, resolver) + } + /// Helper: run a request through the relay and capture what upstream receives. async fn relay_and_capture( raw_header: Vec, @@ -10347,10 +10411,12 @@ mod tests { #[tokio::test] async fn relay_injects_exact_header_credential() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("CUSTOM_TOKEN".to_string(), "tok-12345".to_string())] .into_iter() .collect(), + "api.example.com", + &[("CUSTOM_TOKEN", "header", "x-api-key")], ); let placeholder = child_env.get("CUSTOM_TOKEN").unwrap(); @@ -10424,10 +10490,12 @@ mod tests { #[tokio::test] async fn relay_injects_query_param_credential() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("YOUTUBE_KEY".to_string(), "AIzaSy-secret".to_string())] .into_iter() .collect(), + "www.googleapis.com", + &[("YOUTUBE_KEY", "query", "key")], ); let placeholder = child_env.get("YOUTUBE_KEY").unwrap(); @@ -10535,13 +10603,15 @@ mod tests { #[tokio::test] async fn relay_injects_url_path_credential_telegram_style() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [( "TELEGRAM_TOKEN".to_string(), "123456:ABC-DEF1234ghIkl".to_string(), )] .into_iter() .collect(), + "api.telegram.org", + &[("TELEGRAM_TOKEN", "path", "/bot{credential}/sendMessage")], ); let placeholder = child_env.get("TELEGRAM_TOKEN").unwrap(); @@ -10568,10 +10638,12 @@ mod tests { #[tokio::test] async fn relay_injects_url_path_credential_standalone_segment() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [("ORG_TOKEN".to_string(), "org-abc-789".to_string())] .into_iter() .collect(), + "api.example.com", + &[("ORG_TOKEN", "path", "/api/{credential}/resources")], ); let placeholder = child_env.get("ORG_TOKEN").unwrap(); @@ -10598,13 +10670,18 @@ mod tests { #[tokio::test] async fn relay_injects_combined_path_and_header_credentials() { - let (child_env, resolver) = SecretResolver::from_provider_env( + let (child_env, resolver) = placed_test_resolver( [ ("PATH_TOKEN".to_string(), "tok-path-123".to_string()), ("HEADER_KEY".to_string(), "sk-header-456".to_string()), ] .into_iter() .collect(), + "api.example.com", + &[ + ("PATH_TOKEN", "path", "/bot{credential}/send"), + ("HEADER_KEY", "header", "x-api-key"), + ], ); let path_ph = child_env.get("PATH_TOKEN").unwrap(); let header_ph = child_env.get("HEADER_KEY").unwrap(); diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 772953e302..5370e383c3 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -39,6 +39,7 @@ const OPCODE_BINARY: u8 = 0x2; const OPCODE_CLOSE: u8 = 0x8; const OPCODE_PING: u8 = 0x9; const OPCODE_PONG: u8 = 0xA; +const CREDENTIAL_PLACEMENT_MISMATCH: &str = "websocket credential placement mismatch"; const CREDENTIAL_ENDPOINT_MISMATCH: &str = "websocket credential endpoint mismatch"; #[derive(Clone, Debug)] @@ -582,7 +583,27 @@ where } if let Err(termination) = &result { let error = termination.error.to_string(); - if error.contains(CREDENTIAL_ENDPOINT_MISMATCH) { + if error.contains(CREDENTIAL_PLACEMENT_MISMATCH) { + ocsf_emit!( + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::High) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .firewall_rule(options.policy_name, "credential-binding") + .message("WebSocket credential use denied: unauthorized request location") + .status_detail("credential_placement_mismatch") + .build() + ); + ocsf_emit!(crate::l7::build_credential_placement_mismatch_finding( + options.policy_name, + host, + Some("websocket"), + )); + let _ = write_policy_violation_close(&mut client_write).await; + } else if error.contains(CREDENTIAL_ENDPOINT_MISMATCH) { emit_credential_endpoint_mismatch(host, port, options.policy_name); let _ = write_policy_violation_close(&mut client_write).await; } else if error.contains("credential") { @@ -609,7 +630,7 @@ where } async fn write_policy_violation_close(writer: &mut W) -> Result<()> { - let reason = b"credential endpoint mismatch"; + let reason = b"credential use denied"; let mut frame = Vec::with_capacity(reason.len() + 4); frame.push(0x80 | OPCODE_CLOSE); frame.push(u8::try_from(reason.len() + 2).expect("close reason fits one-byte length")); @@ -1288,7 +1309,12 @@ where resolver .rewrite_websocket_text_placeholders(&mut text) .map_err(|error| { - if error.is_endpoint_mismatch() { + if error.is_placement_mismatch() { + terminate( + WebSocketTerminationCause::PolicyDenial, + miette!(CREDENTIAL_PLACEMENT_MISMATCH), + ) + } else if error.is_endpoint_mismatch() { terminate( WebSocketTerminationCause::PolicyDenial, miette!(CREDENTIAL_ENDPOINT_MISMATCH), @@ -2713,7 +2739,7 @@ network_policies: 1, HashMap::from([("DISCORD_BOT_TOKEN".to_string(), "real-token".to_string())]), HashMap::new(), - HashMap::new(), + crate::test_support::header_placement_metadata(&["DISCORD_BOT_TOKEN"]), HashMap::from([( "DISCORD_BOT_TOKEN".to_string(), StaticCredentialBinding { @@ -2815,7 +2841,7 @@ network_policies: } #[tokio::test] - async fn guarded_websocket_uses_path_scoped_live_resolver() { + async fn guarded_websocket_rewrites_header_credentials_with_policy_opt_in() { let state = bound_websocket_provider_state(); let placeholder = b"openshell:resolve:env:v1_DISCORD_BOT_TOKEN"; let input = masked_frame(true, OPCODE_TEXT, placeholder); @@ -2847,7 +2873,68 @@ network_policies: .await; assert!( result.is_ok(), - "path-scoped live resolver must satisfy the credential guard: {result:?}" + "explicit WebSocket rewriting must permit header credentials: {result:?}" + ); + + drop(relay_write); + let mut output = Vec::new(); + upstream_read.read_to_end(&mut output).await.unwrap(); + assert_eq!(decode_masked_text_frame(&output), "real-token"); + } + + #[tokio::test] + async fn authorization_fallback_preserves_explicit_websocket_rewriting() { + let state = ProviderCredentialState::from_bound_environment( + 1, + HashMap::from([("DISCORD_BOT_TOKEN".to_string(), "real-token".to_string())]), + HashMap::new(), + HashMap::new(), + HashMap::from([( + "DISCORD_BOT_TOKEN".to_string(), + StaticCredentialBinding { + endpoints: vec![StaticCredentialEndpointBinding { + host: "gateway.example.test".into(), + port: 443, + path: "/socket".into(), + }], + credential_identity: "provider:DISCORD_BOT_TOKEN".into(), + workload_credential_handle: String::new(), + }, + )]), + Vec::new(), + ) + .expect("legacy websocket fixture"); + let placeholder = b"openshell:resolve:env:v1_DISCORD_BOT_TOKEN"; + let input = masked_frame(true, OPCODE_TEXT, placeholder); + let (mut client_write, mut relay_read) = tokio::io::duplex(4096); + let (mut relay_write, mut upstream_read) = tokio::io::duplex(4096); + client_write.write_all(&input).await.unwrap(); + drop(client_write); + + let mut options = RelayOptions { + policy_name: "test-policy", + assembly_budget: WebSocketAssemblyBudget::default(), + resolver: None, + generation_guard: None, + provider_credentials: Some(&state), + target: "/socket", + inspector: None, + compression: WebSocketCompression::None, + middleware_session: None, + middleware_context: None, + deny_uninspected_credentials: true, + }; + let result = relay_client_to_server( + &mut relay_read, + &mut relay_write, + "gateway.example.test", + 443, + &mut options, + ) + .await; + assert!( + result.is_ok(), + "Authorization fallback must preserve explicit WebSocket rewriting: {result:?}" ); drop(relay_write); @@ -2914,7 +3001,7 @@ network_policies: result .as_ref() .is_err_and(|error| error.error.to_string().contains("generation changed")), - "revoked credential generation must fail before the frame write: {result:?}" + "revoked generation must fail before the frame write: {result:?}" ); drop(relay_write); diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 6f64d91ee2..85196d9f44 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -6608,7 +6608,10 @@ async fn handle_forward_proxy( .end(openshell_core::proto::MiddlewareSessionEndReason::Cancellation) .await; } - if e.is_endpoint_mismatch() { + if e.is_placement_mismatch() { + crate::l7::relay::reject_credential_resolution(client, &l7_ctx, method, &e).await?; + client.shutdown().await.into_diagnostic()?; + } else if e.is_endpoint_mismatch() { if let Some(observer) = endpoint_observer.as_ref() { observer.observe_credential_failure(true); } @@ -13633,6 +13636,9 @@ network_policies: credentials .resolver .expect("endpoint resolver") + .with_request_location(secrets::RequestCredentialLocation::Header( + "Authorization".into() + )) .resolve_placeholder("openshell:resolve:env:v42_API_TOKEN"), Some("secret") ); diff --git a/crates/openshell-supervisor-network/src/test_support.rs b/crates/openshell-supervisor-network/src/test_support.rs index d1b23b6f31..5c302b91de 100644 --- a/crates/openshell-supervisor-network/src/test_support.rs +++ b/crates/openshell-supervisor-network/src/test_support.rs @@ -1,7 +1,28 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Executable identity and socket-owner subprocess fixtures. +//! Credential, executable identity, and socket-owner subprocess fixtures. + +pub fn header_placement_metadata( + keys: &[&str], +) -> std::collections::HashMap { + keys.iter() + .map(|key| { + ( + format!( + "{}{key}", + openshell_core::provider_credentials::STATIC_CREDENTIAL_METADATA_PREFIX + ), + openshell_core::proto::ProviderProfileCredential { + env_vars: vec![(*key).to_string()], + auth_style: "bearer".to_string(), + header_name: "Authorization".to_string(), + ..Default::default() + }, + ) + }) + .collect() +} #[cfg(target_os = "linux")] use std::net::TcpStream; diff --git a/crates/openshell-supervisor/src/config_runtime.rs b/crates/openshell-supervisor/src/config_runtime.rs index ecda1a7194..02736ad184 100644 --- a/crates/openshell-supervisor/src/config_runtime.rs +++ b/crates/openshell-supervisor/src/config_runtime.rs @@ -2005,6 +2005,9 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + )) .resolve_placeholder(reference), Some("initial"), ); @@ -2076,6 +2079,9 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location(openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + )) .resolve_placeholder(reference), Some("initial"), ); @@ -2628,7 +2634,14 @@ mod tests { let resolved = state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .and_then(|resolver| { - resolver.resolve_placeholder(&reference).map(str::to_owned) + resolver + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ), + ) + .resolve_placeholder(&reference) + .map(str::to_owned) }); let observed = tracker.observation(&state); if state.revision() == revision @@ -2650,6 +2663,11 @@ mod tests { state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .unwrap() + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ) + ) .resolve_placeholder(old_reference), Some("initial") ); @@ -2722,7 +2740,14 @@ mod tests { let resolved = state .resolver_for_endpoint("tools.example.com", 443, "/v1/chat") .and_then(|resolver| { - resolver.resolve_placeholder(&reference).map(str::to_owned) + resolver + .with_request_location( + openshell_core::secrets::RequestCredentialLocation::Header( + "Authorization".into(), + ), + ) + .resolve_placeholder(&reference) + .map(str::to_owned) }); let observed = tracker.observation(&state); if state.revision() == revision diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index a3d4f5245c..66405e5e5b 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -443,8 +443,9 @@ frames or messages from the server. If the endpoint carries provider credentials, OpenShell closes the connection with close code `1008` when the client sends a binary frame, because it cannot check the frame for credentials. Set `allow_uninspected_credentials: true` to relay binary frames anyway. Set -`websocket_credential_rewrite: true` only when client text messages contain -OpenShell credential placeholders that must be resolved. +`websocket_credential_rewrite: true` when client text messages contain credential +placeholders that must be resolved. This explicitly authorizes WebSocket text +rewriting for ordinary static credentials, including credentials with HTTP placement metadata. When new network rules take effect, OpenShell closes open WebSocket connections, so the client must reconnect. The client might receive close code `1012`, or the diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 693efb209c..40faf25379 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -166,8 +166,8 @@ the gateway host. | Field | Type | Default | Description | |---|---|---|---| | `credential_binding.provider` | string | None | Binds the static credentials of an attached provider whose profile defines no endpoints. Valid only in a sandbox policy. | -| `request_body_credential_rewrite` | bool | `false` | Replaces credential placeholders in REST request bodies. | -| `websocket_credential_rewrite` | bool | `false` | Replaces credential placeholders in client WebSocket text messages. | +| `request_body_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in REST bodies, including those with declared HTTP placement. | +| `websocket_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in client WebSocket text messages, including those with declared HTTP placement. | | `allow_uninspected_credentials` | bool | `false` | Allows a provider-credentialed endpoint to omit request inspection or use `tls: skip`. | | `credential_signing` | string | None | AWS request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. | | `signing_service` | string | None | AWS service name, such as `bedrock` or `s3`. Required with `credential_signing`. | diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index bcfd476050..a5299802a7 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -442,15 +442,19 @@ startup, OpenShell replaces each credential with an opaque placeholder token in the agent's environment. When the agent sends an HTTP request containing a placeholder, the proxy resolves it immediately before forwarding the request. -Static credential resolution has two independent authorization boundaries: +Static credential resolution has three independent authorization boundaries: 1. Network policy must allow the calling binary and request destination. 2. The credential binding must include the request host, port, and path. Profile endpoints provide the binding by default. An endpointless profile can use a sandbox policy endpoint that names the attached provider instance through `credential_binding.provider`. +3. The request location must match the declared header, query parameter, or + path-template slot, or an explicitly enabled body/WebSocket rewrite policy. + Without placement metadata, static credentials resolve only in `Authorization` + or through those explicit rewrite policies. -Both checks must pass. A provider profile endpoint does not grant network access +All three checks must pass. A provider profile endpoint does not grant network access unless provider policy composition or the sandbox's own policy allows the request. A plain sandbox policy endpoint does not grant credential use unless the profile already covers it or the endpoint explicitly binds an endpointless @@ -467,21 +471,29 @@ for endpoint matching and migration guidance. ### Supported injection locations -The proxy resolves credential placeholders in the following parts of an HTTP request: +The proxy automatically enforces declared placement when the gateway and +supervisor support it. Moving a header credential into a query parameter, path, +or unrelated header is denied before forwarding. A supporting supervisor defaults +to `Authorization` when placement metadata is absent and rejects malformed +metadata. Custom placements require a supporting gateway and an explicit profile +declaration. Older supervisors keep their previous behavior. | Location | How the agent uses it | Example | |---|---|---| -| Header value | Agent reads `$API_KEY` from env and places it in a header. | `Authorization: Bearer ` | +| Declared header value | Agent reads `$API_KEY` from env and places it in the declared header. | `Authorization: Bearer ` | | Header value (Basic auth) | Agent base64-encodes `user:` in an `Authorization: Basic` header. The proxy decodes, resolves, and re-encodes. | `Authorization: Basic ` | -| Query parameter value | Agent places the placeholder in a URL query parameter. | `GET /api?key=` | -| URL path segment | Agent builds a URL with the placeholder in the path. Supports concatenated patterns. | `POST /bot/sendMessage` | -| Supported request body | An inspected REST endpoint opts in with `request_body_credential_rewrite: true`. | `{"api_key":""}` | -| WebSocket text message | A REST or WebSocket endpoint opts in with `websocket_credential_rewrite: true`. | `{"token":""}` | +| Declared query parameter value | Agent uses the profile's `query_param`. | `GET /api?key=` | +| Declared URL path slot | Agent uses the profile's `path_template`, including concatenated patterns. | `POST /bot/sendMessage` | | AWS SigV4 signing | An endpoint configures `credential_signing`, and the proxy signs with the endpoint-bound AWS credentials. | `credential_signing: sigv4` | The proxy does not rewrite cookies, response content, unsupported request bodies, or WebSocket binary frames. +Explicit endpoint policies can enable credential rewriting in request bodies +or WebSocket text messages. These flags do not authorize arbitrary query +parameters, path slots, or unrelated headers. See [Profiles](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding) +for placement configuration and upgrade requirements. + ### Clients that parse JWT claims A trusted launcher can wrap an issued placeholder in a JWT-shaped alias when a diff --git a/docs/how-it-works/providers/profiles.mdx b/docs/how-it-works/providers/profiles.mdx index 1617e00afe..359b59c486 100644 --- a/docs/how-it-works/providers/profiles.mdx +++ b/docs/how-it-works/providers/profiles.mdx @@ -70,6 +70,7 @@ A request can use a static credential only when all of these checks pass: | The placeholder belongs to the current attached provider state. | Sandbox provider attachment and current provider record. | | The calling binary and destination are allowed. | Effective sandbox network policy. | | The host, port, and canonical request path match the credential binding. | Provider profile endpoints, or an explicit sandbox policy binding for an endpointless profile. | +| The request location matches declared placement, the default `Authorization` header, or an explicit body/WebSocket rewrite policy. | The credential's `auth_style` and its header, query parameter, or path template. | | The HTTP method, path, or protocol operation is allowed when L7 inspection is configured. | Effective sandbox network policy. | | The credential has not expired. | Provider credential expiry metadata. | @@ -94,6 +95,52 @@ glob matching. OpenShell removes the query string and uses a canonical, secret-redacted path for this check, so a credential embedded in a request path does not need to be revealed before authorization. +The supervisor automatically enforces usable static credential auth metadata. +No additional profile flag is required. Header credentials resolve only in the +declared header, query credentials only in the declared parameter, and path +credentials only in the `{credential}` slot of their path template. Header names +are case-insensitive. An `Authorization` binding accepts Bearer, token, and Basic +authentication. + +The credential's `auth_style` selects its placement. Fields for other styles are +ignored, so a bearer credential with unused `query_param` or `path_template` +fields still resolves only in its declared header. An invalid selected placement +is rejected. + +Existing endpoint policies that enable request-body or WebSocket text rewriting +continue to authorize those locations. Those flags do not authorize arbitrary +query parameters, URL paths, or unrelated headers. Trusted AWS signing retains +its existing endpoint authorization. + +Without placement metadata, the supervisor resolves static credentials only in +the `Authorization` header, or through the explicit body/WebSocket policies above. +It rejects malformed placement metadata. A client using `x-api-key`, another +header, a query parameter, or a path slot needs an explicit credential declaration. +For example: + +```yaml showLineNumbers={false} +credentials: + - name: api_key + env_vars: [API_KEY] + auth_style: header + header_name: x-api-key +``` + + +Upgrade the gateway before the supervisor when clients use locations other than +`Authorization`. An older gateway does not forward placement metadata, so a new +supervisor applies the `Authorization` default even if the imported profile +declares another location. Older supervisors keep their previous behavior. + + +Export and update existing imported profiles with the required placement before +upgrading the supervisor. Updating example YAML files does not change profiles +already stored on the gateway. The Codex reference profile declares its access +token and account header; OAuth tokens sent in request bodies still require the +endpoint's explicit body rewrite policy. Trusted AWS signing does not require an +HTTP placement declaration. Credential delivery, wire identities, placeholder +lifecycles, and update behavior remain unchanged. + Use an explicit sandbox policy binding when the profile intentionally defines credentials without defining service endpoints. The policy names the concrete provider instance, not the profile type: @@ -202,7 +249,6 @@ The following provider profile design items are not part of the current behavior | Roadmap item | Current behavior | |---|---| -| General profile-driven credential placement | Static `auth_style`, `header_name`, `query_param`, and `path_template` placement metadata is stored and validated, but static credential injection still depends on environment placeholders generated from provider credentials. Dynamic `token_grant` credentials support `bearer` and `header` placement for matching HTTP endpoints. | | Binary-scoped credential injection | Static placeholder resolution is endpoint-scoped. Dynamic grants in single-route REST and multi-route inspection also require admission by their owning endpoint's policy, including its binary restrictions. L4-only credential forwarding retains endpoint-selector matching. | | Credential verification on create | `openshell provider create` does not yet probe provider verification endpoints or expose `--no-verify`. | | Automatic credential scope extraction | OpenShell does not yet inspect upstream provider responses to discover credential scopes. | @@ -372,12 +418,11 @@ credentials: stable_placeholder: false # Accepted values: basic, bearer, header, query, path. - # These fields describe static credential placement. - # Static runtime injection still uses env placeholder resolution. + # These fields restrict where a static placeholder can resolve. auth_style: bearer header_name: authorization - query_param: api_key - path_template: /v1/{credential}/resources + # For auth_style: query, set query_param: api_key instead. + # For auth_style: path, set path_template: /v1/{credential}/resources instead. refresh: # Accepted values: diff --git a/e2e/python/test_sandbox_providers.py b/e2e/python/test_sandbox_providers.py index 4c39bae95f..6619c4b465 100644 --- a/e2e/python/test_sandbox_providers.py +++ b/e2e/python/test_sandbox_providers.py @@ -236,6 +236,8 @@ def _reply(self): self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(encoded))) + separator = "&" if "?" in self.path else "?" + self.send_header("Link", f"<{self.path}{separator}page=2>; rel=next") self.end_headers() self.wfile.write(encoded) @@ -725,6 +727,163 @@ def call_native_openai(host: str, port: int) -> str: assert body["model"] == "fixture-openai-model" +@pytest.mark.parametrize( + "placement", + ["authorization", "missing", "header", "query", "path", "unused-fields"], +) +def test_static_header_credentials_reject_reflective_request_locations( + sandbox: Callable[..., Sandbox], + sandbox_client: SandboxClient, + placement: str, +) -> None: + """Missing metadata restricts substitution to Authorization and explicit body rewriting.""" + stub = sandbox_client._stub + profile_id = f"e2e-placement-{time.time_ns()}" + secret = "synthetic-placement-secret" + profile = _native_inference_profile( + profile_id=profile_id, + env_var="API_TOKEN", + port=0, + rules=[sandbox_pb2.L7Rule(allow=sandbox_pb2.L7Allow(method="*", path="/**"))], + ) + credential = profile.credentials[0] + if placement == "missing": + credential.auth_style = "" + credential.header_name = "" + elif placement == "unused-fields": + credential.auth_style = "bearer" + credential.header_name = "Authorization" + credential.query_param = "api_key" + credential.path_template = "/v1/{credential}/resources" + elif placement in {"header", "query", "path"}: + credential.auth_style = placement + credential.header_name = "x-api-key" if placement == "header" else "" + credential.query_param = "api_key" if placement == "query" else "" + credential.path_template = ( + "/credential/{credential}/search" if placement == "path" else "" + ) + profile.endpoints[0].request_body_credential_rewrite = True + + def probe_locations(host: str, port: int, placement: str) -> str: + import json + import os + import urllib.error + import urllib.parse + import urllib.request + + token = os.environ["API_TOKEN"] + root = f"http://{host}:{port}" + query_key = "api_key" if placement == "unused-fields" else "probe" + encoded_token = urllib.parse.quote(token, safe="") + token_path = ( + f"/v1/{encoded_token}/resources" + if placement == "unused-fields" + else f"/{encoded_token}/search" + ) + requests = [ + urllib.request.Request( + root + + "/search?" + + urllib.parse.urlencode( + {"q": "rust", "per_page": "1", query_key: token} + ) + ), + urllib.request.Request( + root + "/search", headers={"X-Other-Credential": token} + ), + urllib.request.Request(root + token_path), + urllib.request.Request( + root + "/search", + data=urllib.parse.urlencode({"token": token}).encode(), + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ), + ] + results = [] + for request in requests: + try: + with urllib.request.urlopen(request, timeout=30) as response: + results.append( + { + "status": response.status, + "body": response.read().decode(), + "link": response.headers.get("Link"), + } + ) + except urllib.error.HTTPError as error: + results.append( + { + "status": error.code, + "body": error.read().decode(), + "link": error.headers.get("Link"), + } + ) + target = "/search?q=rust&per_page=1" + headers = {"Authorization": f"token {token}"} + if placement == "header": + headers = {"x-api-key": token} + elif placement == "query": + target += "&" + urllib.parse.urlencode({"api_key": token}) + headers = {} + elif placement == "path": + target = ( + "/credential/" + + urllib.parse.quote(token, safe="") + + "/search?q=rust&per_page=1" + ) + headers = {} + request = urllib.request.Request(root + target, headers=headers) + with urllib.request.urlopen(request, timeout=30) as response: + valid = { + "body": json.loads(response.read()), + "link": response.headers.get("Link"), + } + return json.dumps({"denied": results, "valid": valid}) + + with native_endpoint_server() as port: + profile.endpoints[0].port = port + with ( + imported_provider_profile( + stub, profile=profile, source=f"{profile_id}.yaml" + ), + provider( + stub, + name=f"{profile_id}-provider", + provider_type=profile_id, + credentials={"API_TOKEN": secret}, + profile_workspace="default", + ) as attached_provider, + ): + spec = datamodel_pb2.SandboxSpec( + policy=_default_policy(), providers=[attached_provider] + ) + with sandbox(spec=spec, delete_on_exit=True) as sb: + result = sb.exec_python( + probe_locations, + args=("host.openshell.internal", port, placement), + timeout_seconds=180, + ) + assert result.exit_code == 0, result.stderr + payload = json.loads(result.stdout) + for result in payload["denied"][:3]: + assert result["status"] == 403, result + assert "credential_placement_mismatch" in result["body"] + assert secret not in result["body"] + assert result["link"] is None + body_result = payload["denied"][3] + assert body_result["status"] == 200, body_result + assert secret in body_result["body"] + valid = payload["valid"] + if placement in {"authorization", "missing", "unused-fields"}: + assert valid["body"]["authorization"] == f"token {secret}" + elif placement == "header": + assert valid["body"]["x_api_key"] == secret + else: + assert secret in valid["body"]["path"] + assert "page=2" in valid["link"] + if placement in {"authorization", "missing", "header", "unused-fields"}: + assert secret not in valid["link"] + + def test_imported_anthropic_profile_allows_native_endpoint_with_attached_provider( sandbox: Callable[..., Sandbox], sandbox_client: SandboxClient, diff --git a/providers/aws-s3.yaml b/providers/aws-s3.yaml index 89a60a3f30..e85369951f 100644 --- a/providers/aws-s3.yaml +++ b/providers/aws-s3.yaml @@ -32,6 +32,8 @@ id: aws-s3 display_name: AWS S3 description: AWS S3 storage via STS temporary credentials category: other +# These credentials feed trusted SigV4 signing rather than a direct HTTP +# token header, so no bearer/header placement is declared. credentials: - name: access_key_id description: AWS access key ID (gateway-minted via STS) diff --git a/providers/aws.yaml b/providers/aws.yaml index 087b3ac1d4..f3f1cc568b 100644 --- a/providers/aws.yaml +++ b/providers/aws.yaml @@ -29,6 +29,8 @@ id: aws display_name: AWS description: Generic AWS access via STS temporary credentials category: other +# These credentials feed trusted SigV4 signing rather than a direct HTTP +# token header, so no bearer/header placement is declared. credentials: - name: access_key_id description: AWS access key ID (gateway-minted via STS) diff --git a/providers/codex.yaml b/providers/codex.yaml index bafc5bd063..3698a2bfe4 100644 --- a/providers/codex.yaml +++ b/providers/codex.yaml @@ -33,6 +33,10 @@ credentials: description: Codex OAuth access token env_vars: [CODEX_AUTH_ACCESS_TOKEN] required: true + auth_style: bearer + header_name: authorization + # OAuth refresh uses a request body, not an HTTP authorization header. + # Body substitution requires the endpoint's explicit rewrite policy. - name: refresh_token description: Codex OAuth refresh token env_vars: [CODEX_AUTH_REFRESH_TOKEN] @@ -41,6 +45,9 @@ credentials: description: Codex account identifier env_vars: [CODEX_AUTH_ACCOUNT_ID] required: true + auth_style: header + header_name: chatgpt-account-id + # OIDC identity claims are not an API authorization credential. - name: id_token description: Codex OAuth ID token env_vars: [CODEX_AUTH_ID_TOKEN] diff --git a/skills/debug-inference/SKILL.md b/skills/debug-inference/SKILL.md index 50937104a1..8629499476 100644 --- a/skills/debug-inference/SKILL.md +++ b/skills/debug-inference/SKILL.md @@ -103,8 +103,9 @@ inference request using the provider's documented API shape. | A retired managed endpoint fails DNS resolution | Client still uses the removed managed endpoint | Configure the provider's native base URL and attach an endpoint-bearing provider profile | | Direct request is denied | Missing attachment, endpoint policy, HTTP rule, or binary authorization | Inspect the attached provider profile and sandbox effective policy | | `credential_endpoint_mismatch` | Credential profile does not authorize the request recipient | Correct the host/port/path or import a narrowly scoped profile for the intended endpoint | +| `credential_placement_mismatch` | A placeholder is outside declared placement or the metadata-free `Authorization` default | Match the profile placement. For other headers, query parameters, or path slots, update the imported profile and upgrade the gateway before the supervisor; explicit body/WebSocket rewrite policies remain supported. | | `request_authority_mismatch` | HTTP authority differs from the CONNECT destination | Use the same host and effective port in both authorities | -| Credential variable is absent | Provider was not attached when this process launched, or profiles collide on a key | Attach the provider and launch a new process; resolve duplicate keys explicitly | +| Credential variable is absent | Provider was not attached when this process launched, profiles collide on a key, or credential delivery failed | Check provider readiness, then launch a new process; resolve duplicate keys explicitly. Missing placement metadata or an older component alone does not withhold credentials. | | Upstream rejects the model or body | Client relied on removed model/request rewriting | Configure the real model and provider-native request format in the application | | `127.0.0.1` works on the host but not in the sandbox | Loopback refers to different runtime | Use `host.openshell.internal` or another gateway-reachable endpoint and profile | | Host-local request times out | Server bind address, gateway topology, or host firewall blocks container-to-host traffic | Verify the listener and permit only the required gateway network path and port | diff --git a/skills/openshell-cli/SKILL.md b/skills/openshell-cli/SKILL.md index 351d7e3b84..b757172b7a 100644 --- a/skills/openshell-cli/SKILL.md +++ b/skills/openshell-cli/SKILL.md @@ -150,6 +150,19 @@ when a placeholder is present but requests receive `credential_endpoint_mismatch`. A profileless static provider fails closed because the gateway cannot construct a binding. +Usable credential placement is enforced automatically when both gateway and +supervisor support it. `credential_placement_mismatch` means a placeholder is +outside its declared header, query parameter, or path-template slot. Match the +profile. Explicit body/WebSocket rewrite policies retain their documented +behavior for ordinary credentials. Without placement metadata, a supporting +supervisor permits only `Authorization` and those explicit rewrite policies; +malformed metadata is rejected. For custom headers, query parameters, or path +slots, update the imported profile with explicit placement and upgrade the +gateway before the supervisor. Older gateways cannot forward placement metadata; +older supervisors retain their previous behavior. Updating example YAML does not +update an imported profile. Check provider readiness after changing placement and +launch a new process if retained references were revoked. + When an inspected request receives `request_authority_mismatch`, compare its HTTP authority with the CONNECT tunnel endpoint. The host and effective port must match. For a tunnel to `api.example.com:8443`, send From 4a25a6b34d3cc84b625ccb28a1462a876dc072da Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Fri, 6 Nov 2026 12:56:00 +0000 Subject: [PATCH 14/31] test(conformance): prompt delete when the workload is slow to handle TERM (#5291) * test(conformance): bound running-delete with delayed TERM Signed-off-by: Matthew Grossman * test(conformance): describe slow-TERM delete and lengthen delay Signed-off-by: Matthew Grossman --------- Signed-off-by: Matthew Grossman --- tests/suites/conformance/README.md | 2 +- tests/suites/conformance/src/lifecycle.rs | 68 +++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/tests/suites/conformance/README.md b/tests/suites/conformance/README.md index fe5c95b5f8..c756107cc1 100644 --- a/tests/suites/conformance/README.md +++ b/tests/suites/conformance/README.md @@ -9,7 +9,7 @@ nextest run the tests as the library's unit tests. The crate also builds the | Suite | Covers | |---|---| | `smoke` | Create a sandbox, run `echo` in it, delete it | -| `lifecycle` | SDK: long-running and run-to-completion sandboxes, the default command, restart of a stopped sandbox, deleting a stopped sandbox | +| `lifecycle` | SDK: long-running and run-to-completion sandboxes, prompt delete when the workload is slow to handle TERM, the default command, restart of a stopped sandbox, deleting a stopped sandbox | | `cli` | The same lifecycle through the `openshell` CLI: status, create, get, list, exec, stop, start, delete | | `persistent-workspace` | Files in `/sandbox` survive stop and start | | `policy-advisor` | `policy.local` accepts a proposed rule; a denied address or hostname yields one draft rule | diff --git a/tests/suites/conformance/src/lifecycle.rs b/tests/suites/conformance/src/lifecycle.rs index 02e4f6f117..6af1d145b3 100644 --- a/tests/suites/conformance/src/lifecycle.rs +++ b/tests/suites/conformance/src/lifecycle.rs @@ -7,6 +7,8 @@ //! NVIDIA/OpenShell#4629 by Evan Lezar. Tests that need a driver with a //! persistent workspace live in `persistent_workspace.rs`. +use std::time::Duration; + use testkit::Context; use testkit::sdk::{SandboxPhase, SandboxRestartPolicy}; @@ -40,6 +42,72 @@ async fn long_running_exec_delete(cx: &mut Context) -> testkit::Result { cx.expect_absent(&sandbox).await } +/// Long enough that the workload is still handling TERM when sandbox shutdown +/// begins, and below the supervisor's 5s wait before it kills the workload. +const TERM_HANDLING_DELAY: Duration = Duration::from_secs(3); + +/// Well below the 30s termination grace period that a shutdown hang waits out. +const DELETE_DEADLINE: Duration = Duration::from_secs(10); + +/// Many workloads do not exit immediately on TERM, for example while they run +/// cleanup handlers. Deleting such a running sandbox should still complete +/// promptly, rather than waiting out the runtime's termination grace period. +#[testkit::test] +async fn delete_is_prompt_when_workload_is_slow_to_handle_term( + cx: &mut Context, +) -> testkit::Result { + // `sleep & wait` rather than a foreground `sleep`: sh defers traps until a + // foreground child exits, but `wait` returns on the signal. + let script = format!( + "trap 'sleep {}; exit 0' TERM; \ + touch /tmp/delayed-term-ready; \ + while :; do sleep 3600 & wait $!; done", + TERM_HANDLING_DELAY.as_secs() + ); + let sandbox = cx + .sandbox("dt") + .command(["sh", "-c", script.as_str()]) + .create() + .await?; + sandbox.wait_ready().await?; + + // Ready alone does not prove that the workload has installed its trap. + testkit::poll_until( + "workload TERM handler readiness", + Duration::from_secs(10), + Duration::from_millis(100), + async || { + let output = sandbox + .exec(["sh", "-c", "test -f /tmp/delayed-term-ready"]) + .await?; + Ok(if output.exit_code == 0 { + testkit::Poll::Ready(()) + } else { + testkit::Poll::Pending(format!( + "readiness check exited {}; stderr: {}", + output.exit_code, output.stderr + )) + }) + }, + ) + .await?; + + // Use one deadline starting before delete, not a fresh timeout per step. + // Boundary containment may kill the workload before its handler finishes; + // this asserts bounded deletion, not a graceful workload exit. + tokio::time::timeout(DELETE_DEADLINE, async { + sandbox.delete().await?; + cx.expect_absent(&sandbox).await + }) + .await + .map_err(|_| { + testkit::Error::msg(format!( + "sandbox {} was not fully deleted within {DELETE_DEADLINE:?}", + sandbox.name() + )) + })? +} + /// A sandbox whose main command exits 0, with restart policy Never, reaches /// Completed with exit code 0 and is not restarted. It is gone from get and /// list after delete. From 11fa5f09fef02166fa6a920b141102910faaa6ab Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Fri, 6 Nov 2026 14:23:39 +0100 Subject: [PATCH 15/31] test(tmachine): run Ansible with a dumb terminal (#5307) * test(tmachine): remove systemd OSC context script from guests Fedora's systemd installs /etc/profile.d/80-systemd-osc-context.sh, which makes bash print OSC 3008 markers after every command. Ansible reports them as junk after the JSON data. Remove the script during provisioning. Closes #5306 Signed-off-by: Evan Lezar * test(tmachine): run Ansible with a dumb terminal Fedora's systemd prints OSC 3008 shell-integration markers on terminal sessions: from a profile script, which systemd-tmpfiles recreates on every boot, and from pam_systemd when `become` runs over `ssh -tt`. Ansible reports them as junk after the JSON data. Both emitters skip a dumb terminal, so set TERM=dumb for ansible-playbook instead of changing the guest image. Replaces the cloud-init change, which the next boot undid. Closes #5306 Signed-off-by: Evan Lezar --------- Signed-off-by: Evan Lezar --- tests/tmachine/src/ansible.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/tmachine/src/ansible.rs b/tests/tmachine/src/ansible.rs index 3301934954..c9b37f91f1 100644 --- a/tests/tmachine/src/ansible.rs +++ b/tests/tmachine/src/ansible.rs @@ -28,6 +28,13 @@ pub async fn install_roles() -> Result<()> { pub async fn run(playbook: &Path, inputs: &BTreeMap) -> Result<()> { let mut command = Command::new("ansible-playbook"); + // Newer systemd guests (Fedora) print OSC 3008 shell-integration markers + // on every terminal session: from a profile script for login shells and + // from pam_systemd for `become`, which Ansible runs over `ssh -tt`. + // Ansible reports them as "Module invocation had junk after the JSON + // data". Both emitters skip a dumb terminal, so don't forward the caller's + // TERM. + command.env("TERM", "dumb"); // The VM's SSH forward may not be the inventory's default port. command .arg("--extra-vars") From 20357a9dce053a537f3f2a1786773301110a719f Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Fri, 6 Nov 2026 13:05:36 +0000 Subject: [PATCH 16/31] test(tmachine): prepare runner environments for the e2e/rust migration (#5277) * test(tmachine): load the Python workload image and set the k3s host gateway Every installer loads openshell/e2e-python:dev next to the runtime images, so tests can create sandboxes that run python3, curl, git or openssl. The k3s installers set the chart's server.hostGatewayIP to the node's InternalIP. Tests run on the node, so host.openshell.internal now resolves to it on k3s and k3s-ha. Refs #5275 Signed-off-by: Matthew Grossman * test(testkit): add python_image() for the Python workload image OPENSHELL_TEST_PYTHON_IMAGE overrides the default openshell/e2e-python:dev, next to OPENSHELL_TEST_BASE_IMAGE. Refs #5275 Signed-off-by: Matthew Grossman * test(runner): honor family nextest config and add --test-threads The runner passes its openshell profile as a nextest tool config and the bundle carries tests/suites//.config/nextest.toml into the remapped workspace, so a family can set per-test timeouts and test groups, or override the profile. --test-threads defaults to 1, which keeps the serial --no-capture run. Refs #5275 Signed-off-by: Matthew Grossman * test(features): check host.openshell.internal from a Python sandbox The host-gateway suite starts a server on the host and checks that a sandbox from the Python workload image reaches it through host.openshell.internal. CI runs it on Docker rootful, Podman rootful and rootless, k3s and k3s-ha. Closes #5275 Signed-off-by: Matthew Grossman * test(tmachine): use the k3s node's IPv4 address for the host gateway The node can list an IPv6 InternalIP after the IPv4 one, which made the address discovery retry until it failed. Signed-off-by: Matthew Grossman * test(features): run the host-gateway client with sandbox exec On k3s-ha, attaching to a main process that already finished can reach a gateway replica without the sandbox's session and fail with "sandbox is not ready". Run the client in a Ready sandbox instead. Signed-off-by: Matthew Grossman * Revert "test(runner): honor family nextest config and add --test-threads" This reverts commit 8441388e50a6dbac153b5c9b4a8b18c804bfc9d0. Signed-off-by: Matthew Grossman * test(features): use the system Python for the host-gateway client and trim exports The client needs only the standard library, so run /usr/bin/python3 instead of the image's uv virtual environment and let the policy name its real path. Keep the new image constants private to testkit, and fail the k3s installer if the cluster ever has more than one node, since the host gateway address is the node the tests run on. Refs #5275 Signed-off-by: Matthew Grossman * refactor(testkit): stop exporting environment variable names Tests read the image, CLI binary and gateway settings through functions such as base_image() and python_image(). Nothing outside testkit used the variable-name constants, so keep them private. Refs #5275 Signed-off-by: Matthew Grossman --------- Signed-off-by: Matthew Grossman --- .github/workflows/e2e-suites.yml | 5 + TESTING.md | 7 + tests/suites/features/Cargo.lock | 9 + tests/suites/features/Cargo.toml | 2 +- tests/suites/features/host-gateway/Cargo.toml | 17 ++ .../host-gateway/tests/host_gateway.rs | 209 ++++++++++++++++++ tests/testkit/README.md | 4 + tests/testkit/testkit/src/cli.rs | 2 +- tests/testkit/testkit/src/gateway.rs | 6 +- tests/testkit/testkit/src/image.rs | 27 ++- tests/testkit/testkit/src/lib.rs | 4 +- .../ansible/playbooks/openshell-k3s.yaml | 40 ++++ .../roles/openshell_gateway/tasks/docker.yaml | 9 +- .../roles/openshell_gateway/tasks/podman.yaml | 9 +- tests/tmachine/config.nix | 8 +- 15 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 tests/suites/features/host-gateway/Cargo.toml create mode 100644 tests/suites/features/host-gateway/tests/host_gateway.rs diff --git a/.github/workflows/e2e-suites.yml b/.github/workflows/e2e-suites.yml index 04bd61ef37..791761befb 100644 --- a/.github/workflows/e2e-suites.yml +++ b/.github/workflows/e2e-suites.yml @@ -77,6 +77,11 @@ jobs: {"environment":"ubuntu-docker-rootful","installer":"deb","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootless","installer":"rpm","family":"features","package":"oci-image","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-docker-rootful","installer":"deb","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootful","installer":"rpm","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"fedora-podman-rootless","installer":"rpm","family":"features","package":"host-gateway","gateway":"openshell","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-k3s","installer":"k3s","family":"features","package":"host-gateway","gateway":"tmachine","runner":"ubuntu-24.04"}, + {"environment":"ubuntu-k3s","installer":"k3s-ha","family":"features","package":"host-gateway","gateway":"tmachine","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootless-keycloak","installer":"rpm","family":"features","package":"keycloak-provider-refresh","gateway":"openshell","runner":"ubuntu-24.04"}, {"environment":"fedora-podman-rootful-userns-default","installer":"binaries","family":"drivers/podman","gateway":"tmachine","runner":"ubuntu-24.04"}, diff --git a/TESTING.md b/TESTING.md index 12d81ee984..a3936ad649 100644 --- a/TESTING.md +++ b/TESTING.md @@ -586,6 +586,13 @@ mise run e2e:oci-image:podman Both tasks set `OPENSHELL_TEST_CONTAINER_ENGINE`, the command that builds test images into the gateway's image store. +The `host-gateway` feature suite (`tests/suites/features/host-gateway`) checks +that a sandbox from the Python test image reaches a server the test runs on the +host through `host.openshell.internal`. CI runs it on Docker rootful, Podman +rootful and rootless, and the `k3s` and `k3s-ha` installers. The tests run on +the K3s node, so the K3s installers set the chart's `server.hostGatewayIP` to +the node's address. + Run the VM-backed Rust CLI e2e suite: ```shell diff --git a/tests/suites/features/Cargo.lock b/tests/suites/features/Cargo.lock index c12c38f5ce..bb1a1427ee 100644 --- a/tests/suites/features/Cargo.lock +++ b/tests/suites/features/Cargo.lock @@ -1164,6 +1164,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "host-gateway" +version = "0.0.0" +dependencies = [ + "openshell-testkit", + "tempfile", + "tokio", +] + [[package]] name = "hostname" version = "0.4.2" diff --git a/tests/suites/features/Cargo.toml b/tests/suites/features/Cargo.toml index 5912e9176f..af907b4b15 100644 --- a/tests/suites/features/Cargo.toml +++ b/tests/suites/features/Cargo.toml @@ -3,4 +3,4 @@ [workspace] resolver = "2" -members = ["keycloak-provider-refresh", "kubernetes-updates", "oci-image"] +members = ["host-gateway", "keycloak-provider-refresh", "kubernetes-updates", "oci-image"] diff --git a/tests/suites/features/host-gateway/Cargo.toml b/tests/suites/features/host-gateway/Cargo.toml new file mode 100644 index 0000000000..d81b6cafb2 --- /dev/null +++ b/tests/suites/features/host-gateway/Cargo.toml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "host-gateway" +description = "Features suite: sandboxes reach a host server through host.openshell.internal" +version = "0.0.0" +edition = "2024" +rust-version = "1.94" +license = "Apache-2.0" +repository = "https://github.com/NVIDIA/OpenShell" +publish = false + +[dependencies] +openshell-testkit = { path = "../../../testkit/testkit" } +tempfile = "3" +tokio = { version = "1.43", features = ["io-util", "net", "rt"] } diff --git a/tests/suites/features/host-gateway/tests/host_gateway.rs b/tests/suites/features/host-gateway/tests/host_gateway.rs new file mode 100644 index 0000000000..024b1cd84b --- /dev/null +++ b/tests/suites/features/host-gateway/tests/host_gateway.rs @@ -0,0 +1,209 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! `host.openshell.internal` names the host from inside a sandbox, on every +//! compute driver. +//! +//! The test starts an HTTP server where the tests run, which is the host the +//! gateway runs its sandboxes on, and runs a client in a sandbox from the +//! Python test image ([`testkit::python_image`]) that requests a page from that +//! server through the name. + +use testkit::{Context, Error, Result, ResultExt as _}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinHandle; + +/// The image's system Python, which needs nothing beyond its standard library. +/// The policy names its real path, `/usr/bin/python3.*`, because a rule matches +/// the executable behind a symlink. +const PYTHON: &str = "/usr/bin/python3"; + +/// Requests `/` from `host.openshell.internal` on the port in `argv[1]` and +/// prints the response. A failed connection is retried for 20 seconds, so a +/// slow start doesn't fail the test, and reported on stderr. +const CLIENT: &str = r#" +import socket, sys, time + +host, port = "host.openshell.internal", int(sys.argv[1]) +deadline = time.monotonic() + 20 +while True: + try: + conn = socket.create_connection((host, port), timeout=10) + break + except OSError as error: + print(f"connect {host}:{port}: {error}", file=sys.stderr, flush=True) + if time.monotonic() > deadline: + sys.exit(1) + time.sleep(1) +with conn: + conn.sendall(f"GET / HTTP/1.1\r\nHost: {host}:{port}\r\nConnection: close\r\n\r\n".encode()) + response = b"" + while chunk := conn.recv(4096): + response += chunk +print(response.decode(errors="replace")) +"#; + +/// A sandbox from the Python test image resolves `host.openshell.internal` +/// and gets a response from a server the test runs on the host. The default +/// policy denies all network access, so the test passes one that lets only the +/// image's Python reach the server's port. +#[testkit::test] +async fn python_sandbox_reaches_host_server(cx: &mut Context) -> testkit::Result { + let marker = format!("host-gateway-{}", cx.run_id()); + let server = HostServer::start(marker.clone()).await?; + let policy = PolicyFile::new(&policy(server.port))?; + let port = server.port.to_string(); + let image = testkit::python_image(); + + // The client runs through `sandbox exec` in a Ready sandbox, not as the + // main process. A gateway with several replicas (k3s-ha) can refuse to + // attach to a main process that already finished with "sandbox is not + // ready" when the request reaches a replica that doesn't hold the + // sandbox's session. + let name = cx.name("hg"); + cx.track(&name); + cx.cli([ + "sandbox", + "create", + "--name", + &name, + "--from", + &image, + "--policy", + policy.path()?, + "--detach", + "--no-tty", + "--", + "sh", + "-c", + "exec sleep infinity", + ]) + .await? + .success() + .context(format!("create a sandbox from {image}"))?; + cx.adopt(&name).await?.wait_ready().await?; + + let client = cx + .cli([ + "sandbox", "exec", "--name", &name, "--no-tty", "--", PYTHON, "-c", CLIENT, &port, + ]) + .await? + .success() + .context("run the client in the sandbox")?; + testkit::ensure!( + client.stdout.contains(&marker), + "the client did not get the host server's response:\n{}{}", + client.stdout, + client.stderr + ); + Ok(()) +} + +/// A sandbox policy that allows the image's system Python ([`PYTHON`]) to +/// connect to `host.openshell.internal` on `port`. The endpoint names only the +/// host and port, as the policy docs describe, so the driver's address for the +/// name is what makes it reachable, not an `allowed_ips` range. +fn policy(port: u16) -> String { + format!( + r#"version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /lib64, /proc, /dev/urandom, /etc] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort + +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + host_server: + name: host_server + endpoints: + - host: host.openshell.internal + port: {port} + binaries: + - path: /usr/bin/python3.* +"# + ) +} + +/// An HTTP server on the host that answers every request with `body`. It +/// stops when dropped. +struct HostServer { + port: u16, + task: JoinHandle<()>, +} + +impl HostServer { + /// Listen on every host address, since drivers map + /// `host.openshell.internal` to different ones. + async fn start(body: String) -> Result { + let listener = TcpListener::bind(("0.0.0.0", 0)) + .await + .map_err(|error| Error::from_error(&error)) + .context("bind the host server")?; + let port = listener + .local_addr() + .map_err(|error| Error::from_error(&error)) + .context("read the host server's address")? + .port(); + let task = tokio::spawn(async move { + while let Ok((stream, _)) = listener.accept().await { + tokio::spawn(respond(stream, body.clone())); + } + }); + Ok(Self { port, task }) + } +} + +impl Drop for HostServer { + fn drop(&mut self) { + self.task.abort(); + } +} + +/// Read one request's headers from `stream` and answer with `body`. +async fn respond(mut stream: TcpStream, body: String) { + let _ = stream.set_nodelay(true); + let mut request = Vec::new(); + let mut buf = [0_u8; 1024]; + while !request.windows(4).any(|window| window == b"\r\n\r\n") { + match stream.read(&mut buf).await { + Ok(read) if read > 0 && request.len() < 64 * 1024 => { + request.extend_from_slice(&buf[..read]); + } + _ => return, + } + } + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + let _ = stream.shutdown().await; +} + +/// A sandbox policy written to a temporary file for `sandbox create --policy`. +struct PolicyFile(tempfile::NamedTempFile); + +impl PolicyFile { + fn new(contents: &str) -> Result { + let file = tempfile::NamedTempFile::new() + .map_err(|error| Error::from_error(&error)) + .context("create policy file")?; + std::fs::write(file.path(), contents) + .map_err(|error| Error::from_error(&error)) + .context(format!("write {}", file.path().display()))?; + Ok(Self(file)) + } + + fn path(&self) -> Result<&str> { + let path = self.0.path(); + path.to_str() + .ok_or_else(|| Error::msg(format!("{} is not UTF-8", path.display()))) + } +} diff --git a/tests/testkit/README.md b/tests/testkit/README.md index 3f786fe65d..c02ad01c25 100644 --- a/tests/testkit/README.md +++ b/tests/testkit/README.md @@ -75,6 +75,10 @@ fails. A killed test process skips cleanup, so names are unique to the run. compute driver. Driver suites call it first. - `testkit::base_image()` for the image to create sandboxes from or build on: `OPENSHELL_TEST_BASE_IMAGE`, or OpenShell's default sandbox image. +- `testkit::python_image()` for sandboxes that run `python3`, curl, git, or + openssl: `OPENSHELL_TEST_PYTHON_IMAGE`, or `openshell/e2e-python:dev` + (`e2e/python/Dockerfile.workload`). The tmachine installers load it into + every environment; `mise run e2e:workload:build` builds it locally. - `cx.cli([...])` to run the installed CLI against the same gateway, with `success()`, `failure()`, and `json()` on the output. `cx.cli_handle()` returns that CLI for code that can't borrow `cx`, such as a cleanup hook. diff --git a/tests/testkit/testkit/src/cli.rs b/tests/testkit/testkit/src/cli.rs index abba6065c3..5ac54e4108 100644 --- a/tests/testkit/testkit/src/cli.rs +++ b/tests/testkit/testkit/src/cli.rs @@ -21,7 +21,7 @@ use crate::gateway::{ENDPOINT_ENV, GATEWAY_ENV, Gateway, WORKSPACE_ENV}; /// How long one CLI invocation may take. pub const CLI_TIMEOUT: Duration = Duration::from_mins(5); /// The CLI binary to run. Defaults to `openshell` on PATH. -pub const BIN_ENV: &str = "OPENSHELL_BIN"; +pub(crate) const BIN_ENV: &str = "OPENSHELL_BIN"; /// Runs the CLI against the gateway and workspace the tests use. #[derive(Clone, Debug)] diff --git a/tests/testkit/testkit/src/gateway.rs b/tests/testkit/testkit/src/gateway.rs index ee3e333500..6513fcfc4e 100644 --- a/tests/testkit/testkit/src/gateway.rs +++ b/tests/testkit/testkit/src/gateway.rs @@ -21,9 +21,9 @@ use openshell_sdk::{ClientConfig, ClientIdentity, Health, OpenShellClient, Servi use crate::error::{Error, Result, ResultExt}; -pub const ENDPOINT_ENV: &str = "OPENSHELL_GATEWAY_ENDPOINT"; -pub const GATEWAY_ENV: &str = "OPENSHELL_GATEWAY"; -pub const WORKSPACE_ENV: &str = "OPENSHELL_WORKSPACE"; +pub(crate) const ENDPOINT_ENV: &str = "OPENSHELL_GATEWAY_ENDPOINT"; +pub(crate) const GATEWAY_ENV: &str = "OPENSHELL_GATEWAY"; +pub(crate) const WORKSPACE_ENV: &str = "OPENSHELL_WORKSPACE"; const DEFAULT_WORKSPACE: &str = "default"; // Mirrors the SDK's endpoint settings for channels testkit builds itself. diff --git a/tests/testkit/testkit/src/image.rs b/tests/testkit/testkit/src/image.rs index 4e398800b1..26bb2278fe 100644 --- a/tests/testkit/testkit/src/image.rs +++ b/tests/testkit/testkit/src/image.rs @@ -1,18 +1,35 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! The base image tests create sandboxes from or build their images on. +//! The images tests create sandboxes from or build their images on. /// Overrides [`DEFAULT_BASE_IMAGE`], e.g. with a mirror. -pub const BASE_IMAGE_ENV: &str = "OPENSHELL_TEST_BASE_IMAGE"; +const BASE_IMAGE_ENV: &str = "OPENSHELL_TEST_BASE_IMAGE"; /// OpenShell's built-in default sandbox image. Pulling it needs access to /// nvcr.io. -pub const DEFAULT_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; +const DEFAULT_BASE_IMAGE: &str = "nvcr.io/nvidia/base/ubuntu:24.04"; + +/// Overrides [`DEFAULT_PYTHON_IMAGE`]. +const PYTHON_IMAGE_ENV: &str = "OPENSHELL_TEST_PYTHON_IMAGE"; +/// The workload image for tests that run `python3`, curl, git or openssl in a +/// sandbox, built from `e2e/python/Dockerfile.workload`. It isn't published: +/// the tmachine installers load it into every environment, and +/// `mise run e2e:workload:build` builds it locally. +const DEFAULT_PYTHON_IMAGE: &str = "openshell/e2e-python:dev"; /// The image named by [`BASE_IMAGE_ENV`], or [`DEFAULT_BASE_IMAGE`]. pub fn base_image() -> String { - std::env::var(BASE_IMAGE_ENV) + image_from_env(BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE) +} + +/// The image named by [`PYTHON_IMAGE_ENV`], or [`DEFAULT_PYTHON_IMAGE`]. +pub fn python_image() -> String { + image_from_env(PYTHON_IMAGE_ENV, DEFAULT_PYTHON_IMAGE) +} + +fn image_from_env(var: &str, default: &str) -> String { + std::env::var(var) .ok() .filter(|image| !image.trim().is_empty()) - .unwrap_or_else(|| DEFAULT_BASE_IMAGE.to_string()) + .unwrap_or_else(|| default.to_string()) } diff --git a/tests/testkit/testkit/src/lib.rs b/tests/testkit/testkit/src/lib.rs index dee03cac53..834f28c1e7 100644 --- a/tests/testkit/testkit/src/lib.rs +++ b/tests/testkit/testkit/src/lib.rs @@ -39,14 +39,14 @@ mod registry; mod run; pub use cleanup::CLEANUP_TIMEOUT; -pub use cli::{BIN_ENV, CLI_TIMEOUT, Cli, CliOutput}; +pub use cli::{CLI_TIMEOUT, Cli, CliOutput}; pub use context::{ Context, DELETE_TIMEOUT, EXEC_TIMEOUT, ExecOutput, READY_TIMEOUT, STOP_TIMEOUT, Sandbox, SandboxBuilder, }; pub use error::{Error, Result, ResultExt}; pub use gateway::Gateway; -pub use image::{BASE_IMAGE_ENV, DEFAULT_BASE_IMAGE, base_image}; +pub use image::{base_image, python_image}; pub use openshell_sdk as sdk; pub use poll::{Poll, poll_until}; pub use program::main; diff --git a/tests/tmachine/ansible/playbooks/openshell-k3s.yaml b/tests/tmachine/ansible/playbooks/openshell-k3s.yaml index 2f03f9d7a8..5b68bc0067 100644 --- a/tests/tmachine/ansible/playbooks/openshell-k3s.yaml +++ b/tests/tmachine/ansible/playbooks/openshell-k3s.yaml @@ -34,6 +34,9 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + # The testkit's python_image() sandbox workload. + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" - name: Import OpenShell images into K3s ansible.builtin.command: @@ -49,6 +52,7 @@ - gateway - sandbox - supervisor + - e2e-python - name: Copy OpenShell Helm chart ansible.builtin.copy: @@ -56,6 +60,41 @@ dest: /var/lib/openshell/artifacts/helm-chart.tgz mode: "0600" + # Tests run in the guest, so host.openshell.internal must resolve to the + # node itself. A tmachine environment is one VM, so the cluster has one + # node; the check below fails if that stops being true, because then the + # address must be the one of the node the tests run on. QEMU user + # networking gives every boot the same guest address (10.0.2.15), so the + # value stays valid when the disk is reused. + # The node can also list an IPv6 address after it, so take the IPv4 one. + - name: Discover the K3s node address + ansible.builtin.command: + argv: + - /usr/local/bin/k3s + - kubectl + - --request-timeout=10s + - get + - nodes + - --output=jsonpath={.items[0].status.addresses[?(@.type=="InternalIP")].address} + register: k3s_node_address + changed_when: false + until: >- + k3s_node_address.rc == 0 + and k3s_node_address.stdout.split() | select('match', '^[0-9.]+$') | list | length > 0 + retries: 30 + delay: 2 + + - name: Check the K3s cluster has a single node + ansible.builtin.command: + argv: [/usr/local/bin/k3s, kubectl, --request-timeout=10s, get, nodes, --output=name] + register: k3s_nodes + changed_when: false + failed_when: k3s_nodes.rc != 0 or k3s_nodes.stdout_lines | length != 1 + + - name: Select the K3s node IPv4 address + ansible.builtin.set_fact: + k3s_node_ipv4: "{{ k3s_node_address.stdout.split() | select('match', '^[0-9.]+$') | first }}" + - name: Write OpenShell Helm values ansible.builtin.copy: dest: /var/lib/openshell/artifacts/values.yaml @@ -85,6 +124,7 @@ auth: allowUnauthenticatedUsers: true disableTls: true + hostGatewayIP: "{{ k3s_node_ipv4 }}" telemetryEnabled: false - name: Write OpenShell Helm values overlay diff --git a/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml b/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml index 3585c1d33f..11b033972a 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml +++ b/tests/tmachine/ansible/roles/openshell_gateway/tasks/docker.yaml @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 --- -- name: Copy OpenShell runtime images +# Tests run sandboxes from the e2e-python workload image (the testkit's +# python_image()), so load it next to the runtime images. +- name: Copy OpenShell runtime and test workload images become: true ansible.builtin.copy: src: "{{ item.src }}" @@ -15,8 +17,10 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" -- name: Load OpenShell runtime images +- name: Load OpenShell runtime and test workload images become: true ansible.builtin.command: argv: @@ -27,3 +31,4 @@ loop: - sandbox - supervisor + - e2e-python diff --git a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml index 784c56f6cb..b85361b769 100644 --- a/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml +++ b/tests/tmachine/ansible/roles/openshell_gateway/tasks/podman.yaml @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 --- -- name: Copy OpenShell runtime images +# Tests run sandboxes from the e2e-python workload image (the testkit's +# python_image()), so load it next to the runtime images. +- name: Copy OpenShell runtime and test workload images become: true ansible.builtin.copy: src: "{{ item.src }}" @@ -15,8 +17,10 @@ src: "{{ openshell_sandbox_image }}" - name: supervisor src: "{{ openshell_supervisor_image }}" + - name: e2e-python + src: "{{ openshell_e2e_python_image }}" -- name: Load OpenShell runtime images +- name: Load OpenShell runtime and test workload images become: true become_user: "{{ openshell_gateway_user }}" ansible.builtin.command: @@ -28,6 +32,7 @@ loop: - sandbox - supervisor + - e2e-python environment: HOME: "{{ openshell_gateway_home }}" XDG_RUNTIME_DIR: "/run/user/{{ openshell_gateway_uid }}" diff --git a/tests/tmachine/config.nix b/tests/tmachine/config.nix index ac1aa28d6d..24206097f9 100644 --- a/tests/tmachine/config.nix +++ b/tests/tmachine/config.nix @@ -23,9 +23,12 @@ let OVMF = firmwarePkgs.OVMF; }; qemu = qemuPkgs.qemu.override { hostCpuOnly = true; }; + # Every installer loads the sandbox workload image tests run python3 in. + e2ePythonImage = "../../artifacts/images/openshell-e2e-python-dev.tar"; k3sInputs = { agent_sandbox_version = "0.5.0"; openshell_cli_binary = "../../artifacts/binaries/${muslTarget}/openshell"; + openshell_e2e_python_image = e2ePythonImage; openshell_gateway_image = "../../artifacts/images/openshell-gateway-tmachine.tar"; openshell_helm_chart = "../../artifacts/helm/helm-chart-0.0.0.tgz"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; @@ -204,6 +207,7 @@ let inputs = { openshell_cli_binary = "../../artifacts/binaries/${muslTarget}/openshell"; openshell_gateway_binary = "../../artifacts/binaries/${gnuTarget}/openshell-gateway"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -216,6 +220,7 @@ let ]; inputs = { openshell_deb = "../../artifacts/packages/openshell.deb"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -227,6 +232,7 @@ let inputs = { openshell_rpm = "../../artifacts/packages/rpm/openshell.rpm"; openshell_gateway_rpm = "../../artifacts/packages/rpm/openshell-gateway.rpm"; + openshell_e2e_python_image = e2ePythonImage; openshell_supervisor_image = "../../artifacts/images/openshell-supervisor-tmachine.tar"; openshell_sandbox_image = "../../artifacts/images/openshell-sandbox-tmachine.tar"; }; @@ -245,7 +251,7 @@ let playbooks = [ "ansible/playbooks/drivers/podman/e2e.yaml" ]; inputs = { openshell_podman_e2e_test_bundle = "../../artifacts/test-archives/${muslTarget}/openshell-podman-e2e-tests.tar"; - openshell_podman_e2e_workload_image = "../../artifacts/images/openshell-e2e-python-dev.tar"; + openshell_podman_e2e_workload_image = e2ePythonImage; }; } ]; From e9ba8d9e3568f7ca1a6320914d0b17e0572fac2d Mon Sep 17 00:00:00 2001 From: Divesh <47188680+FrostGod@users.noreply.github.com> Date: Fri, 6 Nov 2026 14:04:51 +0000 Subject: [PATCH 17/31] fix(sandbox): bound runtime shutdown after SIGTERM (#5279) * fix(sandbox): bound runtime shutdown after SIGTERM After a termination signal the boundary kills the workload and stops serving, then drops its process runtime. The drop waited without a limit for every blocking task. The supervisor keeps an agent `Wait` exchange open for the life of the sandbox, and that exchange blocks on a condition variable that only the agent's async reaper wakes. When the runtime shutdown cancelled the reaper before it recorded the exit, the `Wait` never returned and the process stayed alive until SIGKILL, so every delete or stop took the full termination grace period. Stop the process runtime with a two-second bound on every return path from `serve`. Workload kill and reaping are unchanged. Part of #5025 Signed-off-by: divesh * test(sandbox): bound the whole SIGTERM exit test Run the child through run_isolated so a stall during setup fails the test instead of hanging it, and have the fake supervisor send SIGTERM once its exchanges are parked. Signed-off-by: divesh --------- Signed-off-by: divesh --- .../openshell-sandbox/src/boundary_server.rs | 252 +++++++++++++++++- 1 file changed, 251 insertions(+), 1 deletion(-) diff --git a/crates/openshell-sandbox/src/boundary_server.rs b/crates/openshell-sandbox/src/boundary_server.rs index 2c7b957707..a01f3b797e 100644 --- a/crates/openshell-sandbox/src/boundary_server.rs +++ b/crates/openshell-sandbox/src/boundary_server.rs @@ -97,6 +97,10 @@ mod linux { /// Bound the wait for a flush before acknowledging termination. A timed-out /// blocking task cannot be cancelled and may outlive the acknowledgement. const FILESYSTEM_FLUSH_TIMEOUT: Duration = Duration::from_secs(3); + /// Bound the process runtime's shutdown after the boundary stops serving. + /// A blocking task such as a supervisor's agent `Wait` can outlive the + /// termination that would end it, and the shutdown cannot cancel it. + const PROCESS_RUNTIME_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2); const MAX_PENDING_HANDSHAKES: usize = 32; const MAX_CONTROL_CONNECTIONS: usize = 128; /// Bounds one runtime-initiated dial, including finding the supervisor @@ -297,7 +301,17 @@ mod linux { launcher, qualification, )?); - serve(&config.listener, runtime) + serve_then_stop(process_runtime, &config.listener, runtime) + } + + fn serve_then_stop( + process_runtime: tokio::runtime::Runtime, + config: &BoundaryListenerConfig, + runtime: Arc, + ) -> Result<(), String> { + let result = serve(config, runtime); + process_runtime.shutdown_timeout(PROCESS_RUNTIME_SHUTDOWN_TIMEOUT); + result } fn make_boundary_nondumpable() -> Result<(), String> { @@ -6232,6 +6246,242 @@ mod linux { ); } + /// Send one logical request on its own exchange and return the reply. + async fn exchange_request( + channel: tonic::transport::Channel, + handle: &str, + request: Request, + token: &str, + ) -> Response { + let request = RequestEnvelope::new(request).expect("encode request"); + let frames = tokio_stream::iter([ + openshell_sandbox_backend::isolation_protocol::exchange_frame(handle), + openshell_sandbox_backend::isolation_protocol::request_chunk(&request) + .expect("typed logical request"), + ]); + let mut body = SandboxProtocolClient::new(channel) + .exchange(bearer_request(frames, token)) + .await + .expect("exchange logical request") + .into_inner(); + let mut frame = Vec::new(); + while let Some(chunk) = body.message().await.expect("read gRPC response") { + frame.extend_from_slice(&chunk_data(chunk).unwrap()); + } + openshell_sandbox_backend::boundary_protocol::decode_frame::(&frame) + .expect("decode logical response") + .response + } + + /// A shared supervisor outlives the sandboxes it serves and keeps + /// their exchanges open, so SIGTERM must end the process without + /// waiting for the supervisor to close them. + #[test] + fn sigterm_exits_while_the_supervisor_holds_an_exchange_open() { + const CHILD_MARKER: &str = "OPENSHELL_TEST_BOUNDARY_SIGTERM_CHILD"; + const EXIT_BUDGET: Duration = Duration::from_secs(10); + if std::env::var_os(CHILD_MARKER).is_none() { + run_isolated( + "boundary_server::linux::tests::sigterm_exits_while_the_supervisor_holds_an_exchange_open", + CHILD_MARKER, + Duration::from_mins(1), + ); + return; + } + + let directory = tempfile::tempdir().expect("temporary directory"); + let (server_tls, client_tls) = stage_test_tls(directory.path(), "sigterm"); + let socket_path = directory.path().join("supervisor.sock"); + let listener = BoundaryListenerConfig::RuntimeInitiatedUnix { + socket_path: socket_path.clone(), + tls: server_tls, + }; + let supervisor_runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(1) + .enable_all() + .build() + .expect("supervisor runtime"); + let supervisor_listener = { + let _entered = supervisor_runtime.enter(); + tokio::net::UnixListener::bind(&socket_path).expect("bind supervisor socket") + }; + let process_runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("test process runtime"); + let (network_broker, workload_launcher) = test_network_broker(); + let (verification_key, token) = test_auth_material("sigterm"); + let boundary = { + let _entered = process_runtime.enter(); + Arc::new( + BoundaryRuntime::new( + BoundaryConfig { + boundary_id: "sigterm".to_string(), + generation: "generation-1".to_string(), + session_id: test_session_id(), + session_rotation: openshell_core::jwt::SessionRotation::new(1) + .expect("session rotation"), + auth_epoch: CredentialEpoch::new(1).expect("auth epoch"), + gateway_id: "test-gateway".to_string(), + verification_keys: vec![verification_key], + listener: listener.clone(), + resource_claims: std::collections::BTreeMap::new(), + resource_claim_files: std::collections::BTreeMap::new(), + workload_identity: test_workload_identity(), + provisioning_evidence: test_provisioning_evidence(), + child_env: std::collections::HashMap::new(), + }, + process_runtime.handle().clone(), + network_broker, + workload_launcher, + test_runtime_qualification(), + ) + .expect("test boundary runtime"), + ) + }; + let opening = test_open_request(&boundary.config); + let (signalled_sender, signalled) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let supervisor = std::panic::AssertUnwindSafe(|| { + supervisor_runtime.block_on(async move { + let (stream, _) = supervisor_listener.accept().await.expect("runtime dial"); + let server_name = + rustls::pki_types::ServerName::try_from(client_tls.server_name.clone()) + .expect("valid server name"); + let stream = tokio_rustls::TlsConnector::from(Arc::new( + test_client_config(&client_tls), + )) + .connect(server_name, stream) + .await + .expect("verify runtime TLS"); + let mut stream = Some(stream); + let channel = tonic::transport::Endpoint::from_static( + "http://boundary.openshell.internal", + ) + .connect_with_connector(tower::service_fn( + move |_: tonic::transport::Uri| { + let stream = stream.take(); + async move { + stream.map(hyper_util::rt::TokioIo::new).ok_or_else(|| { + io::Error::other("the runtime connection is in use") + }) + } + }, + )) + .await + .expect("gRPC over the runtime connection"); + let handle = test_open_boundary(channel.clone(), opening, &token).await; + let attach = Request::Attach { + supervisor_instance_id: test_supervisor_instance_id(), + policy: Box::new(takeover_test_policy()), + supervisor_takeover: None, + supervisor_recovery_timeout_secs: None, + }; + assert!(matches!( + exchange_request(channel.clone(), &handle, attach, &token).await, + Response::Attached { .. } + )); + assert!(matches!( + exchange_request(channel.clone(), &handle, Request::Confirm, &token) + .await, + Response::Confirmed { .. } + )); + let start = Request::StartAgent { + sandbox_id: "sigterm".to_string(), + spec: AgentSpecWire { + program: "/bin/sleep".to_string(), + args: vec!["300".to_string()], + workdir: None, + timeout_secs: 0, + interactive: false, + otlp_relay_active: false, + }, + ca_cert: None, + ca_bundle: None, + provider_env_revision: 0, + provider_env: std::collections::HashMap::new(), + provider_files: std::collections::HashMap::new(), + }; + let process_id = + match exchange_request(channel.clone(), &handle, start, &token).await { + Response::Started { process_id, .. } => process_id, + response => panic!("the agent did not start: {response:?}"), + }; + // The supervisor waits for the agent and for workload + // connections for as long as the sandbox runs. + let mut parked = Vec::new(); + for request in [Request::Wait { process_id }, Request::AcceptNetwork] { + let (sender, receiver) = tokio::sync::mpsc::channel(4); + sender + .send( + openshell_sandbox_backend::isolation_protocol::exchange_frame( + &handle, + ), + ) + .await + .expect("bind exchange"); + sender + .send( + openshell_sandbox_backend::isolation_protocol::request_chunk( + &RequestEnvelope::new(request).expect("encode request"), + ) + .expect("typed request"), + ) + .await + .expect("send request"); + let pending = SandboxProtocolClient::new(channel.clone()) + .exchange(bearer_request(ReceiverStream::new(receiver), &token)) + .await + .expect("open exchange") + .into_inner(); + parked.push((sender, pending)); + } + for (_, pending) in &mut parked { + assert!( + tokio::time::timeout(Duration::from_millis(500), pending.message()) + .await + .is_err(), + "a parked exchange answered before termination" + ); + } + signalled_sender + .send(std::time::Instant::now()) + .expect("report SIGTERM time"); + nix::sys::signal::kill( + nix::unistd::Pid::this(), + nix::sys::signal::Signal::SIGTERM, + ) + .expect("send SIGTERM"); + std::future::pending::<()>().await; + drop(parked); + }); + }); + if std::panic::catch_unwind(supervisor).is_err() { + std::process::exit(1); + } + }); + + // The parked `Wait` blocks shutdown only when the shutdown cancels + // the agent's reaper before it records the exit, so a task that + // never ends stands in for that loss. + process_runtime.spawn_blocking(|| { + loop { + std::thread::park(); + } + }); + install_boundary_signal_handlers().expect("install termination handlers"); + serve_then_stop(process_runtime, &listener, boundary) + .expect("terminate after supervisor confirmation"); + let elapsed = signalled + .recv() + .expect("the supervisor sent SIGTERM") + .elapsed(); + assert!( + elapsed < EXIT_BUDGET, + "the boundary took {elapsed:?} to stop after SIGTERM" + ); + } + #[tokio::test(flavor = "multi_thread")] async fn grpc_blackhole_expires_connection_and_releases_mediation_lease() { let (runtime, token) = availability_test_runtime(); From 638974575f1454d9d49fd63dabf4b022ce7a314e Mon Sep 17 00:00:00 2001 From: Harry Turner <51107404+HarryMoss@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:32:59 +0000 Subject: [PATCH 18/31] feat(ocsf): correlate audit events with sampled OpenTelemetry traces (#4789) * feat(ocsf): correlate audit events with sampled OpenTelemetry traces Add optional trace.uid enrichment through a plain sampled-context extractor, with explicit correlation taking precedence on all ten event builders. Register the extractor at gateway and supervisor startup and keep an INFO span active around network and HTTP deny emissions at the default log level. Integrate the Authentication event and current network APIs while retaining sanitization, policy decisions, credential handling and conditional DEBUG parenting. Document per-decision traces and export/retention limits. Signed-off-by: Harry Turner Signed-off-by: Kris Hicks * refactor(ocsf): validate trace correlation IDs TraceCorrelation accepted any string in a field documented as a W3C trace ID, and every builder exposed a .trace() override that only tests called. Build the correlation from the 16-byte trace ID, reject malformed or all-zero IDs on deserialization, and drop the builder setters so the registered extractor is the only source of trace.uid. Signed-off-by: Kris Hicks * fix(ocsf): limit trace correlation to HTTP and API Activity OCSF 1.8.0 declares the Trace profile only for HTTP Activity and API Activity, but trace.uid was added to every class, validated against schemas composed by grafting http_activity onto eight other classes. Strict consumers reject such records outright. Gate enrichment on the event class, drop the composed schemas and their tooling, and vendor only the native HTTP/API +trace exports. The supervisor now opens a deny span only for decisions with an HTTP Activity record, so every exported deny span has a record pointing at it; the wiring that served Network Activity and finding-only denials is removed. Signed-off-by: Kris Hicks * refactor(supervisor-network): derive deny classification from the event Whether a decision opened a deny span came from three places: the event action, a denied: macro arm, and a flag passed to emit_decision. Several refusals were recorded only by those flags, so the audit records themselves did not say the request was refused. Mark the refusal in the records instead: fail-closed response middleware invocations, fail-closed and transformed-body policy failure findings, and enforced response delivery failures now carry action Denied. Deny classification reads only the records, and the flags are gone. Signed-off-by: Kris Hicks * chore(supervisor-network): reject direct ocsf_emit! in the egress crate An HTTP deny emitted with openshell_ocsf::ocsf_emit! instead of egress_emit! silently gets no deny span, and the crate still used the upstream macro in several places. Disallow it in the crate clippy config and route every emission through egress_emit!. Signed-off-by: Kris Hicks * feat(supervisor-network): rate-limit deny spans per destination Every denied HTTP decision exported a root span, and the workload controls how often it is denied. A tight retry loop against a blocked host could fill the batch exporter queue and push out the supervisor's other spans. Give each destination a token bucket of 10 deny spans that refills at one per second, tracking at most 1024 destinations. Denials past the limit still emit their OCSF records, without a trace. Signed-off-by: Kris Hicks * feat(otel): set trace context on OCSF log records OCSF records exported over OTLP carried trace correlation only in the JSON body, and only for HTTP and API Activity. OTLP backends link logs to traces through the log record's own trace and span ID fields. Fill those fields from the sampled span active at emission. They are OTLP fields, not OCSF attributes, so every class gets them without affecting schema conformance; Network Activity and findings regain trace links on the OTLP path. Signed-off-by: Kris Hicks * test(supervisor-network): use an in-memory span exporter for deny-span tests Each deny-span test started a loopback OTLP gRPC server and waited for a batch export, which made the suite slow. Capture spans with an in-memory exporter instead, converted to the same OTLP span shape, and keep two tests that export over OTLP end to end. Signed-off-by: Kris Hicks * docs(ocsf): describe which records report a refusal Several records that describe a refusal gained action Denied in this branch: FORWARD parse, canonicalization, GraphQL and JSON-RPC rejections, enforced L7 evaluation errors, metadata-server 403/405 responses, and fail-closed middleware and response delivery records. These shift deny counts in a SIEM, so list them where operators look for denied records. Signed-off-by: Kris Hicks * refactor(supervisor-network): reduce deny spans to one entry point The deny-span API still carried paths for nested and non-HTTP deny scopes that no caller reaches now that only HTTP decisions get spans, and a decision whose first HTTP record was allowed opened its deny span with that record's destination and no reason. emit_decision is now the single entry point, behind a one-arm egress_emit!. It anchors the span on the denied HTTP record and sets optional attributes directly. Records that never share a span are emitted inline again, response diagnostics return their events so each error path adds its own terminal failure, and the trace tests share deny-span helpers and fixtures. Signed-off-by: Kris Hicks * fix(supervisor-network): keep FORWARD denials in the connect trace At debug level, FORWARD credential-binding and request middleware denials opened root deny spans, because the saved connect span exists there but is not entered. Deny spans also charged the per-destination limiter even when they were disabled, and a fail-closed WebSocket middleware finding did not record the denial its HTTP counterparts do. Those denials now pass or run under the saved connect span, the limiter is consulted only when INFO spans are enabled, and the fail-closed WebSocket finding sets action Denied. Signed-off-by: Kris Hicks * fix(supervisor-network): classify refusals and bound deny spans Mark remaining inspection, capacity, admission and response refusals as Denied while preserving fail-open and I/O error classification. Add a process-wide deny-span budget alongside destination limits, with audit-preservation tests and matching documentation. Signed-off-by: Harry Turner --------- Signed-off-by: Harry Turner Signed-off-by: Kris Hicks Co-authored-by: Kris Hicks --- AGENTS.md | 1 + Cargo.lock | 2 + ...container+host+security_control+trace.json | 8273 ++++++++++++++++ ...ity@ai_operation+container+host+trace.json | 8138 ++++++++++++++++ ...operation+host+security_control+trace.json | 8241 ++++++++++++++++ .../api_activity@ai_operation+host+trace.json | 8106 ++++++++++++++++ ...container+host+security_control+trace.json | 8265 ++++++++++++++++ .../api_activity@container+host+trace.json | 8130 ++++++++++++++++ ..._activity@host+security_control+trace.json | 8233 ++++++++++++++++ .../v1.8.0/api_activity@host+trace.json | 8098 ++++++++++++++++ ...+network_proxy+security_control+trace.json | 8326 +++++++++++++++++ .../openshell-ocsf/src/events/base_event.rs | 30 +- crates/openshell-ocsf/src/events/mod.rs | 16 + crates/openshell-ocsf/src/lib.rs | 3 + crates/openshell-ocsf/src/trace.rs | 226 + .../src/tracing_layers/event_bridge.rs | 6 +- crates/openshell-otel/src/correlation.rs | 27 + crates/openshell-otel/src/lib.rs | 24 + crates/openshell-otel/src/ocsf.rs | 91 +- .../openshell-otel/tests/ocsf_correlation.rs | 228 + crates/openshell-server/src/cli.rs | 4 + .../openshell-supervisor-network/Cargo.toml | 2 + .../openshell-supervisor-network/clippy.toml | 6 + .../src/google_cloud_metadata.rs | 37 +- .../openshell-supervisor-network/src/host.rs | 12 +- .../src/l7/middleware.rs | 273 +- .../src/l7/mod.rs | 7 +- .../src/l7/relay.rs | 126 +- .../src/l7/rest.rs | 257 +- .../src/l7/rest/http_response.rs | 409 +- .../src/l7/token_grant_injection.rs | 7 +- .../src/l7/websocket.rs | 20 +- .../openshell-supervisor-network/src/lib.rs | 1 + .../openshell-supervisor-network/src/opa.rs | 4 +- .../src/policy_dns/mod.rs | 11 +- .../src/policy_dns/runtime.rs | 6 +- .../src/policy_dns/store.rs | 2 +- .../src/policy_local.rs | 8 +- .../openshell-supervisor-network/src/proxy.rs | 277 +- .../src/proxy/tests/compatibility.rs | 1 + .../src/proxy/trace_tests.rs | 246 + .../openshell-supervisor-network/src/run.rs | 8 +- .../src/telemetry.rs | 222 + .../src/telemetry/tests.rs | 602 ++ crates/openshell-supervisor/src/main.rs | 26 +- docs/how-it-works/gateways/configuration.mdx | 1 + docs/observability/ocsf-json-export.mdx | 31 + 47 files changed, 76738 insertions(+), 332 deletions(-) create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json create mode 100644 crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json create mode 100644 crates/openshell-ocsf/src/trace.rs create mode 100644 crates/openshell-otel/src/correlation.rs create mode 100644 crates/openshell-otel/tests/ocsf_correlation.rs create mode 100644 crates/openshell-supervisor-network/clippy.toml create mode 100644 crates/openshell-supervisor-network/src/proxy/trace_tests.rs create mode 100644 crates/openshell-supervisor-network/src/telemetry.rs create mode 100644 crates/openshell-supervisor-network/src/telemetry/tests.rs diff --git a/AGENTS.md b/AGENTS.md index 2ba6597b9c..8866ead489 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,6 +153,7 @@ ocsf_emit!(event); - `crate::ocsf_ctx()` returns the process-wide `EventContext`. It is always available (falls back to defaults in tests). - `ocsf_emit!()` is non-blocking and cannot panic. It stores the event in a thread-local and emits via `tracing::info!()`. +- In `openshell-supervisor-network`, emit with `crate::telemetry::egress_emit!` instead, or `crate::telemetry::emit_decision` for the records of one decision, so denied HTTP decisions get a deny span. Clippy rejects `ocsf_emit!` there. - The shorthand layer and JSONL layer extract the event from the thread-local. The shorthand format is derived automatically from the builder fields. - For security findings, **dual-emit**: one domain event (e.g., `SshActivityBuilder`) AND one `DetectionFindingBuilder` for the same incident. - Never log secrets, credentials, or query parameters in OCSF messages. The OCSF JSONL file may be shipped to external systems. diff --git a/Cargo.lock b/Cargo.lock index 3932bee044..94cc7b3df6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5333,11 +5333,13 @@ dependencies = [ "openshell-ocsf", "openshell-ocsf-validate", "openshell-otel", + "openshell-otel-test-support", "openshell-policy", "openshell-policy-schema", "openshell-sandbox-backend", "openshell-supervisor-middleware", "openshell-supervisor-middleware-builtins", + "opentelemetry-proto", "opentelemetry_sdk", "prost-types", "rcgen", diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json new file mode 100644 index 0000000000..873d9a449c --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+security_control+trace.json @@ -0,0 +1,8273 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json new file mode 100644 index 0000000000..a0a313d187 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+container+host+trace.json @@ -0,0 +1,8138 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json new file mode 100644 index 0000000000..6c8cf83eb1 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+security_control+trace.json @@ -0,0 +1,8241 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json new file mode 100644 index 0000000000..ee9a0d4e14 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@ai_operation+host+trace.json @@ -0,0 +1,8106 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "message_context": { + "$ref": "#/$defs/message_context", + "title": "Message Context" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "ai_model": { + "$ref": "#/$defs/ai_model", + "title": "AI Model" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json new file mode 100644 index 0000000000..f982941183 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+security_control+trace.json @@ -0,0 +1,8265 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json new file mode 100644 index 0000000000..7c1523afd3 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@container+host+trace.json @@ -0,0 +1,8130 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json new file mode 100644 index 0000000000..1c2e700d12 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+security_control+trace.json @@ -0,0 +1,8233 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json new file mode 100644 index 0000000000..d1315dd7c0 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/api_activity@host+trace.json @@ -0,0 +1,8098 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "resource_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "criticality": { + "title": "Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Resource Relationship" + }, + "role": { + "title": "Role", + "type": "string" + }, + "role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Role ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Resource Details", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "message_context": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "application" + ] + }, + { + "required": [ + "service" + ] + } + ], + "properties": { + "ai_role": { + "title": "AI Role", + "type": "string" + }, + "ai_role_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "AI Role ID", + "type": "integer" + }, + "application": { + "$ref": "#/$defs/application", + "title": "Application" + }, + "completion_tokens": { + "title": "Completion Tokens", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "prompt_tokens": { + "title": "Prompt Tokens", + "type": "integer" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "total_tokens": { + "title": "Total Tokens", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Message Context", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "graph": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "edges": { + "items": { + "$ref": "#/$defs/edge" + }, + "title": "Edges", + "type": "array" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "nodes": { + "items": { + "$ref": "#/$defs/node" + }, + "title": "Nodes", + "type": "array" + }, + "query_language": { + "title": "Query Language", + "type": "string" + }, + "query_language_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Query Language ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "nodes" + ], + "title": "Graph", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "application": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "criticality": { + "title": "Business Criticality", + "type": "string" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Application Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Application Name", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "resource_relationship": { + "$ref": "#/$defs/graph", + "title": "Application Relationship" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "sbom": { + "$ref": "#/$defs/sbom", + "title": "Related SBOM" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Application Type", + "type": "string" + }, + "uid": { + "title": "Application ID", + "type": "string" + }, + "uid_alt": { + "title": "Application Alternative ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "version": { + "title": "Application Version", + "type": "string" + } + }, + "title": "Application", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "edge": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "is_directed": { + "title": "Directed", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "relation": { + "title": "Relation", + "type": "string" + }, + "source": { + "title": "Source", + "type": "string" + }, + "target": { + "title": "Target", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "source", + "target" + ], + "title": "Edge", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "ai_model": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ai_provider": { + "title": "AI Provider", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "ai_provider" + ], + "title": "AI Model", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "sbom": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "package": { + "$ref": "#/$defs/package", + "title": "Software Package" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "software_components": { + "items": { + "$ref": "#/$defs/software_component" + }, + "title": "Software Components", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "SBOM ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "package", + "software_components" + ], + "title": "Software Bill of Materials", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "software_component": { + "additionalProperties": false, + "properties": { + "author": { + "title": "Author", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "related_component": { + "title": "Related Component", + "type": "string" + }, + "relationship": { + "title": "Relationship", + "type": "string" + }, + "relationship_id": { + "enum": [ + 0, + 1, + 99 + ], + "title": "Relationship ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Component", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "node": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Node", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/api_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "category_uid": { + "const": 6, + "title": "Category ID", + "type": "integer" + }, + "api": { + "$ref": "#/$defs/api", + "title": "API Details" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "resources": { + "items": { + "$ref": "#/$defs/resource_details" + }, + "title": "Resources Array", + "type": "array" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "class_uid": { + "const": 6003, + "title": "Class ID", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + } + }, + "required": [ + "activity_id", + "actor", + "api", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "src_endpoint", + "time", + "type_uid" + ], + "title": "API Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json b/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json new file mode 100644 index 0000000000..654712b727 --- /dev/null +++ b/crates/openshell-ocsf-validate/schemas/v1.8.0/http_activity@container+host+network_proxy+security_control+trace.json @@ -0,0 +1,8326 @@ +{ + "$defs": { + "kb_article": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "src_url" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "KB Article", + "type": "object" + }, + "long_string": { + "additionalProperties": false, + "properties": { + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Long String", + "type": "object" + }, + "enrichment": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "short_desc": { + "title": "Short Description", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "data", + "name", + "value" + ], + "title": "Enrichment", + "type": "object" + }, + "ldap_person": { + "additionalProperties": false, + "properties": { + "cost_center": { + "title": "Cost Center", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "deleted_time": { + "title": "Deleted Time", + "type": "integer" + }, + "department": { + "title": "Department", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "email_addrs": { + "items": { + "type": "string" + }, + "title": "Email Addresses", + "type": "array" + }, + "employee_uid": { + "title": "Employee ID", + "type": "string" + }, + "given_name": { + "title": "Given Name", + "type": "string" + }, + "hire_time": { + "title": "Hire Time", + "type": "integer" + }, + "job_title": { + "title": "Job Title", + "type": "string" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "last_login_time": { + "title": "Last Login", + "type": "integer" + }, + "ldap_cn": { + "title": "LDAP Common Name", + "type": "string" + }, + "ldap_dn": { + "title": "LDAP Distinguished Name", + "type": "string" + }, + "leave_time": { + "title": "Leave Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "manager": { + "$ref": "#/$defs/user", + "title": "Manager" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "office_location": { + "title": "Office Location", + "type": "string" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "surname": { + "title": "Surname", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + } + }, + "title": "LDAP Person", + "type": "object" + }, + "email_auth": { + "additionalProperties": false, + "properties": { + "dkim": { + "title": "DKIM Status", + "type": "string" + }, + "dkim_domain": { + "title": "DKIM Domain", + "type": "string" + }, + "dkim_signature": { + "title": "DKIM Signature", + "type": "string" + }, + "dmarc": { + "title": "DMARC Status", + "type": "string" + }, + "dmarc_override": { + "title": "DMARC Override", + "type": "string" + }, + "dmarc_policy": { + "title": "DMARC Policy", + "type": "string" + }, + "spf": { + "title": "SPF Status", + "type": "string" + } + }, + "title": "Email Authentication", + "type": "object" + }, + "epss": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "percentile": { + "title": "EPSS Percentile", + "type": "number" + }, + "score": { + "title": "EPPS Score", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "score" + ], + "title": "EPSS", + "type": "object" + }, + "endpoint_connection": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "code" + ] + }, + { + "required": [ + "network_endpoint" + ] + } + ], + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "network_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Network Endpoint" + } + }, + "title": "Endpoint Connection", + "type": "object" + }, + "device_hw_info": { + "additionalProperties": false, + "properties": { + "bios_date": { + "title": "BIOS Date", + "type": "string" + }, + "bios_manufacturer": { + "title": "BIOS Manufacturer", + "type": "string" + }, + "bios_ver": { + "title": "BIOS Version", + "type": "string" + }, + "chassis": { + "title": "Chassis", + "type": "string" + }, + "cpu_architecture": { + "title": "CPU Architecture", + "type": "string" + }, + "cpu_architecture_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "CPU Architecture ID", + "type": "integer" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "cpu_cores": { + "title": "CPU Cores", + "type": "integer" + }, + "cpu_count": { + "title": "CPU Count", + "type": "integer" + }, + "cpu_speed": { + "title": "Processor Speed", + "type": "integer" + }, + "cpu_type": { + "title": "Processor Type", + "type": "string" + }, + "desktop_display": { + "$ref": "#/$defs/display", + "title": "Desktop Display" + }, + "gpu_count": { + "title": "GPU Count", + "type": "integer" + }, + "gpu_info_list": { + "items": { + "$ref": "#/$defs/gpu_info" + }, + "title": "GPU information", + "type": "array" + }, + "keyboard_info": { + "$ref": "#/$defs/keyboard_info", + "title": "Keyboard Information" + }, + "ram_size": { + "title": "RAM Size", + "type": "integer" + }, + "serial_number": { + "title": "Serial Number", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Device Hardware Info", + "type": "object" + }, + "network_proxy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Proxy Endpoint", + "type": "object" + }, + "data_classification": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "category_id" + ] + }, + { + "required": [ + "confidentiality_id" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "category_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Category ID", + "type": "integer" + }, + "classifier_details": { + "$ref": "#/$defs/classifier_details", + "title": "Classifier Details" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "discovery_details": { + "items": { + "$ref": "#/$defs/discovery_details" + }, + "title": "Discovery Details", + "type": "array" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + }, + "status_details": { + "items": { + "type": "string" + }, + "title": "Status Details", + "type": "array" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "total": { + "title": "Total", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Data Classification", + "type": "object" + }, + "http_response": { + "additionalProperties": false, + "properties": { + "body_length": { + "title": "Response Body Length", + "type": "integer" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "content_type": { + "title": "HTTP Content Type", + "type": "string" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "latency": { + "title": "Latency", + "type": "integer" + }, + "length": { + "title": "Response Length", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "status": { + "title": "Status", + "type": "string" + } + }, + "required": [ + "code" + ], + "title": "HTTP Response", + "type": "object" + }, + "display": { + "additionalProperties": false, + "properties": { + "color_depth": { + "title": "Color Depth", + "type": "integer" + }, + "physical_height": { + "title": "Physical Height", + "type": "integer" + }, + "physical_orientation": { + "title": "Physical Orientation", + "type": "integer" + }, + "physical_width": { + "title": "Physical Width", + "type": "integer" + }, + "scale_factor": { + "title": "Scale Factor", + "type": "integer" + } + }, + "title": "Display", + "type": "object" + }, + "scim": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "error_message": { + "title": "Last Error Message", + "type": "string" + }, + "is_group_provisioning_enabled": { + "title": "SCIM Group Provisioning Enabled", + "type": "boolean" + }, + "is_user_provisioning_enabled": { + "title": "SCIM User Provisioning Enabled", + "type": "boolean" + }, + "last_run_time": { + "title": "Last Sync Time", + "type": "integer" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "scim_group_schema": { + "title": "SCIM Group Schema" + }, + "scim_user_schema": { + "title": "SCIM User Schema" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "External ID", + "type": "string" + }, + "url_string": { + "title": "SCIM Endpoint URL", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + }, + "version": { + "title": "SCIM Version", + "type": "string" + } + }, + "title": "SCIM", + "type": "object" + }, + "programmatic_credential": { + "additionalProperties": false, + "properties": { + "last_used_time": { + "title": "Last Used Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Programmatic Credential", + "type": "object" + }, + "os": { + "additionalProperties": false, + "properties": { + "build": { + "title": "OS Build", + "type": "string" + }, + "country": { + "title": "Country", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "cpu_bits": { + "title": "CPU Bits", + "type": "integer" + }, + "edition": { + "title": "OS Edition", + "type": "string" + }, + "kernel_release": { + "title": "Kernel Release", + "type": "string" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "sp_name": { + "title": "OS Service Pack", + "type": "string" + }, + "sp_ver": { + "title": "OS Service Pack Version", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 99, + 100, + 101, + 200, + 201, + 300, + 301, + 302, + 400, + 401, + 402 + ], + "title": "Type ID", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "Operating System (OS)", + "type": "object" + }, + "tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Tactic", + "type": "object" + }, + "dns_answer": { + "additionalProperties": false, + "properties": { + "class": { + "title": "Resource Record Class", + "type": "string" + }, + "flag_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "type": "integer" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "DNS Header Flags", + "type": "array" + }, + "packet_uid": { + "title": "Packet UID", + "type": "integer" + }, + "rdata": { + "title": "DNS RData", + "type": "string" + }, + "ttl": { + "title": "TTL", + "type": "integer" + }, + "type": { + "title": "Resource Record Type", + "type": "string" + } + }, + "required": [ + "rdata" + ], + "title": "DNS Answer", + "type": "object" + }, + "discovery_details": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "occurrence_details": { + "$ref": "#/$defs/occurrence_details", + "title": "Occurrence Details" + }, + "occurrences": { + "items": { + "$ref": "#/$defs/occurrence_details" + }, + "title": "Occurrences", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "title": "Discovery Details", + "type": "object" + }, + "sso": { + "additionalProperties": false, + "properties": { + "auth_protocol": { + "title": "Auth Protocol", + "type": "string" + }, + "auth_protocol_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 99 + ], + "title": "Auth Protocol ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "SAML Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "duration_mins": { + "title": "SSO Session Duration", + "type": "integer" + }, + "idle_timeout": { + "title": "SSO Idle Timeout", + "type": "integer" + }, + "login_endpoint": { + "title": "SSO Login Endpoint", + "type": "string" + }, + "logout_endpoint": { + "title": "SSO Logout Endpoint", + "type": "string" + }, + "metadata_endpoint": { + "title": "SSO Metadata Endpoint", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scopes": { + "items": { + "type": "string" + }, + "title": "Scopes", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "vendor_name": { + "title": "Service Provider", + "type": "string" + } + }, + "title": "SSO", + "type": "object" + }, + "token": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_renewable": { + "title": "Renewable", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + } + }, + "title": "Token", + "type": "object" + }, + "api": { + "additionalProperties": false, + "properties": { + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "request": { + "$ref": "#/$defs/request", + "title": "API Request Details" + }, + "response": { + "$ref": "#/$defs/response", + "title": "API Response Details" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "token": { + "$ref": "#/$defs/token", + "title": "Token" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "operation" + ], + "title": "API", + "type": "object" + }, + "d3fend": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "d3f_tactic" + ] + }, + { + "required": [ + "d3f_technique" + ] + } + ], + "properties": { + "d3f_tactic": { + "$ref": "#/$defs/d3f_tactic", + "title": "MITRE D3FEND\u2122 Tactic" + }, + "d3f_technique": { + "$ref": "#/$defs/d3f_technique", + "title": "MITRE D3FEND\u2122 Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122", + "type": "object" + }, + "reputation": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Reputation Score", + "type": "number" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "score": { + "title": "Reputation Score", + "type": "string" + }, + "score_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "Reputation Score ID", + "type": "integer" + } + }, + "required": [ + "base_score", + "score_id" + ], + "title": "Reputation", + "type": "object" + }, + "mitigation": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "countermeasures": { + "items": { + "$ref": "#/$defs/d3fend" + }, + "title": "Countermeasures", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Mitigation", + "type": "object" + }, + "location": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "city" + ] + }, + { + "required": [ + "country" + ] + }, + { + "required": [ + "postal_code" + ] + }, + { + "required": [ + "region" + ] + } + ], + "properties": { + "aerial_height": { + "title": "Aerial Height", + "type": "string" + }, + "city": { + "title": "City", + "type": "string" + }, + "continent": { + "title": "Continent", + "type": "string" + }, + "coordinates": { + "items": { + "type": "number" + }, + "title": "Coordinates", + "type": "array" + }, + "country": { + "title": "Country", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "geodetic_altitude": { + "title": "Geodetic Altitude", + "type": "string" + }, + "geodetic_vertical_accuracy": { + "title": "Geodetic Vertical Accuracy", + "type": "string" + }, + "geohash": { + "title": "Geohash", + "type": "string" + }, + "horizontal_accuracy": { + "title": "Horizontal Accuracy", + "type": "string" + }, + "is_on_premises": { + "title": "On-Premises", + "type": "boolean" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "lat": { + "title": "Latitude", + "type": "number" + }, + "long": { + "title": "Longitude", + "type": "number" + }, + "postal_code": { + "title": "Postal Code", + "type": "string" + }, + "pressure_altitude": { + "title": "Pressure Altitude", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + } + }, + "title": "Geo Location", + "type": "object" + }, + "network_traffic": { + "additionalProperties": false, + "properties": { + "bytes": { + "title": "Total Bytes", + "type": "integer" + }, + "bytes_in": { + "title": "Bytes In", + "type": "integer" + }, + "bytes_missed": { + "title": "Bytes Missed", + "type": "integer" + }, + "bytes_out": { + "title": "Bytes Out", + "type": "integer" + }, + "chunks": { + "title": "Chunks", + "type": "integer" + }, + "chunks_in": { + "title": "Chunks In", + "type": "integer" + }, + "chunks_out": { + "title": "Chunks Out", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "packets": { + "title": "Total Packets", + "type": "integer" + }, + "packets_in": { + "title": "Packets In", + "type": "integer" + }, + "packets_out": { + "title": "Packets Out", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "timespan": { + "$ref": "#/$defs/timespan", + "title": "Time Span" + } + }, + "title": "Network Traffic", + "type": "object" + }, + "idp": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "auth_factors": { + "items": { + "$ref": "#/$defs/auth_factor" + }, + "title": "Authentication Factors", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "fingerprint": { + "$ref": "#/$defs/fingerprint", + "title": "Certificate Fingerprint" + }, + "has_mfa": { + "title": "MFA Enforced", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "protocol_name": { + "title": "Supported Protocol", + "type": "string" + }, + "scim": { + "$ref": "#/$defs/scim", + "title": "SCIM" + }, + "sso": { + "$ref": "#/$defs/sso", + "title": "SSO" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "Configuration URL", + "type": "string" + } + }, + "title": "Identity Provider", + "type": "object" + }, + "process": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "ancestry": { + "items": { + "$ref": "#/$defs/process_entity" + }, + "title": "Ancestry", + "type": "array" + }, + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "environment_variables": { + "items": { + "$ref": "#/$defs/environment_variable" + }, + "title": "Environment Variables", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hosted_services": { + "items": { + "$ref": "#/$defs/win_win_service" + }, + "title": "Hosted Services", + "type": "array" + }, + "integrity": { + "title": "Integrity", + "type": "string" + }, + "integrity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Integrity Level", + "type": "integer" + }, + "lineage": { + "items": { + "type": "string" + }, + "title": "Lineage", + "type": "array" + }, + "loaded_modules": { + "items": { + "type": "string" + }, + "title": "Loaded Modules", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "parent_process": { + "$ref": "#/$defs/process", + "title": "Parent Process" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "ptid": { + "title": "Process Thread ID", + "type": "integer" + }, + "sandbox": { + "title": "Sandbox", + "type": "string" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "terminated_time": { + "title": "Terminated Time", + "type": "integer" + }, + "tid": { + "title": "Thread ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + }, + "working_directory": { + "title": "Working Directory", + "type": "string" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + } + }, + "title": "Process", + "type": "object" + }, + "vulnerability": { + "additionalProperties": false, + "oneOf": [ + { + "not": { + "required": [ + "cve", + "cwe" + ] + }, + "required": [ + "advisory" + ] + }, + { + "not": { + "required": [ + "advisory", + "cwe" + ] + }, + "required": [ + "cve" + ] + }, + { + "not": { + "required": [ + "advisory", + "cve" + ] + }, + "required": [ + "cwe" + ] + } + ], + "properties": { + "advisory": { + "$ref": "#/$defs/advisory", + "title": "Security Advisory" + }, + "affected_code": { + "items": { + "$ref": "#/$defs/affected_code" + }, + "title": "Affected Code", + "type": "array" + }, + "affected_packages": { + "items": { + "$ref": "#/$defs/affected_package" + }, + "title": "Affected Software Packages", + "type": "array" + }, + "category": { + "title": "Category", + "type": "string" + }, + "cve": { + "$ref": "#/$defs/cve", + "title": "CVE" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "dependency_chain": { + "title": "Dependency Chain", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "exploit_last_seen_time": { + "title": "Exploit Last Seen Time", + "type": "integer" + }, + "exploit_ref_url": { + "title": "Exploit URL", + "type": "string" + }, + "exploit_requirement": { + "title": "Exploit Requirement", + "type": "string" + }, + "exploit_type": { + "title": "Exploit Type", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "fix_coverage": { + "title": "Fix Coverage", + "type": "string" + }, + "fix_coverage_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Fix Coverage ID", + "type": "integer" + }, + "is_exploit_available": { + "title": "Exploit Availability", + "type": "boolean" + }, + "is_fix_available": { + "title": "Fix Availability", + "type": "boolean" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "packages": { + "items": { + "$ref": "#/$defs/package" + }, + "title": "Software Packages", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_vulnerabilities": { + "items": { + "type": "string" + }, + "title": "Related Vulnerability IDs", + "type": "array" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + } + }, + "title": "Vulnerability Details", + "type": "object" + }, + "affected_code": { + "additionalProperties": false, + "properties": { + "end_column": { + "title": "End Column", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "rule": { + "$ref": "#/$defs/rule", + "title": "Related Rule" + }, + "start_column": { + "title": "Start Column", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "required": [ + "file" + ], + "title": "Affected Code", + "type": "object" + }, + "keyboard_info": { + "additionalProperties": false, + "properties": { + "function_keys": { + "title": "Function Keys", + "type": "integer" + }, + "ime": { + "title": "IME", + "type": "string" + }, + "keyboard_layout": { + "title": "Keyboard Layout", + "type": "string" + }, + "keyboard_subtype": { + "title": "Keyboard Subtype", + "type": "integer" + }, + "keyboard_type": { + "title": "Keyboard Type", + "type": "string" + } + }, + "title": "Keyboard Information", + "type": "object" + }, + "certificate": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "is_self_signed": { + "title": "Certificate Self-Signed", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Distinguished Name", + "type": "string" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "serial_number": { + "title": "Certificate Serial Number", + "type": "string" + }, + "subject": { + "title": "Subject Distinguished Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "issuer", + "serial_number" + ], + "title": "Digital Certificate", + "type": "object" + }, + "account": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "is_disabled": { + "title": "Disabled", + "type": "boolean" + }, + "is_locked": { + "title": "Locked", + "type": "boolean" + }, + "is_on_premises_sync_enabled": { + "title": "On-Premises Sync Enabled", + "type": "boolean" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Account", + "type": "object" + }, + "authorization": { + "additionalProperties": false, + "properties": { + "decision": { + "title": "Authorization Decision/Outcome", + "type": "string" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "title": "Authorization Result", + "type": "object" + }, + "autonomous_system": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "number" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "number": { + "title": "Number", + "type": "integer" + } + }, + "title": "Autonomous System", + "type": "object" + }, + "classifier_details": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "Classifier Details", + "type": "object" + }, + "session": { + "additionalProperties": false, + "properties": { + "count": { + "title": "Count", + "type": "integer" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "expiration_reason": { + "title": "Expiration Reason", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "is_mfa": { + "title": "Multi Factor Authentication", + "type": "boolean" + }, + "is_remote": { + "title": "Remote", + "type": "boolean" + }, + "is_vpn": { + "title": "VPN Session", + "type": "boolean" + }, + "issuer": { + "title": "Issuer Details", + "type": "string" + }, + "terminal": { + "title": "Terminal", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "uuid": { + "title": "UUID", + "type": "string" + } + }, + "title": "Session", + "type": "object" + }, + "d3f_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Technique", + "type": "object" + }, + "image": { + "additionalProperties": false, + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Image", + "type": "object" + }, + "script": { + "additionalProperties": false, + "properties": { + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "script_content": { + "$ref": "#/$defs/long_string", + "title": "Script Content" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "script_content", + "type_id" + ], + "title": "Script", + "type": "object" + }, + "affected_package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "fixed_in_version": { + "title": "Fixed In Version", + "type": "string" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "remediation": { + "$ref": "#/$defs/remediation", + "title": "Remediation Guidance" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Affected Software Package", + "type": "object" + }, + "cis_control": { + "additionalProperties": false, + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "CIS Control", + "type": "object" + }, + "analytic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "State ID", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Analytic", + "type": "object" + }, + "actor": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "app_name" + ] + }, + { + "required": [ + "app_uid" + ] + }, + { + "required": [ + "invoked_by" + ] + }, + { + "required": [ + "process" + ] + }, + { + "required": [ + "session" + ] + }, + { + "required": [ + "user" + ] + } + ], + "properties": { + "app_name": { + "title": "Application Name", + "type": "string" + }, + "app_uid": { + "title": "Application ID", + "type": "string" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "idp": { + "$ref": "#/$defs/idp", + "title": "Identity Provider" + }, + "invoked_by": { + "title": "Invoked by", + "type": "string" + }, + "process": { + "$ref": "#/$defs/process", + "title": "Process" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "user": { + "$ref": "#/$defs/user", + "title": "User" + } + }, + "title": "Actor", + "type": "object" + }, + "cloud": { + "additionalProperties": false, + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "cloud_partition": { + "title": "Cloud Partition", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "project_uid": { + "title": "Project ID", + "type": "string" + }, + "provider": { + "title": "Cloud Provider", + "type": "string" + }, + "region": { + "title": "Region", + "type": "string" + }, + "zone": { + "title": "Cloud Availability Zone", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "Cloud", + "type": "object" + }, + "trace": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "span": { + "$ref": "#/$defs/span", + "title": "Span" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Trace", + "type": "object" + }, + "file": { + "additionalProperties": false, + "properties": { + "parent_folder": { + "title": "Parent Folder", + "type": "string" + }, + "is_system": { + "title": "System", + "type": "boolean" + }, + "xattributes": { + "$ref": "#/$defs/object", + "title": "Extended Attributes" + }, + "confidentiality_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Confidentiality ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "accessor": { + "$ref": "#/$defs/user", + "title": "Accessor" + }, + "is_readonly": { + "title": "Read-Only", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "ext": { + "title": "File Extension", + "type": "string" + }, + "company_name": { + "title": "Company Name", + "type": "string" + }, + "attributes": { + "title": "Attributes", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uri": { + "title": "File URI", + "type": "string" + }, + "is_deleted": { + "title": "Deleted", + "type": "boolean" + }, + "confidentiality": { + "title": "Confidentiality", + "type": "string" + }, + "drive_type": { + "title": "Drive Type", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "imported_symbols": { + "items": { + "type": "string" + }, + "title": "Imported Symbols", + "type": "array" + }, + "signature": { + "$ref": "#/$defs/digital_signature", + "title": "Digital Signature" + }, + "is_encrypted": { + "title": "Encrypted", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "accessed_time": { + "title": "Accessed Time", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "internal_name": { + "title": "Internal Name", + "type": "string" + }, + "hashes": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Hashes", + "type": "array" + }, + "drive_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Drive Type ID", + "type": "integer" + }, + "security_descriptor": { + "title": "Security Descriptor", + "type": "string" + }, + "volume": { + "title": "Volume", + "type": "string" + }, + "modifier": { + "$ref": "#/$defs/user", + "title": "Modifier" + }, + "mime_type": { + "title": "MIME type", + "type": "string" + }, + "encryption_details": { + "$ref": "#/$defs/encryption_details", + "title": "Encryption Details" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Digital Signatures", + "type": "array" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "type_id" + ], + "title": "File", + "type": "object" + }, + "remediation": { + "additionalProperties": false, + "properties": { + "cis_controls": { + "items": { + "$ref": "#/$defs/cis_control" + }, + "title": "CIS Controls", + "type": "array" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "kb_article_list": { + "items": { + "$ref": "#/$defs/kb_article" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "kb_articles": { + "items": { + "type": "string" + }, + "title": "Knowledgebase Articles", + "type": "array" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + } + }, + "required": [ + "desc" + ], + "title": "Remediation", + "type": "object" + }, + "email": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "from" + ] + }, + { + "required": [ + "to" + ] + } + ], + "properties": { + "cc": { + "items": { + "type": "string" + }, + "title": "Cc", + "type": "array" + }, + "cc_mailboxes": { + "items": { + "type": "string" + }, + "title": "Cc Mailboxes", + "type": "array" + }, + "delivered_to": { + "title": "Delivered To", + "type": "string" + }, + "delivered_to_list": { + "items": { + "type": "string" + }, + "title": "Delivered To List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "from": { + "title": "From", + "type": "string" + }, + "from_list": { + "items": { + "type": "string" + }, + "title": "From List", + "type": "array" + }, + "from_mailbox": { + "title": "From Mailbox", + "type": "string" + }, + "from_mailboxes": { + "items": { + "type": "string" + }, + "title": "From Mailboxes", + "type": "array" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "is_read": { + "title": "Read", + "type": "boolean" + }, + "message_uid": { + "title": "Message UID", + "type": "string" + }, + "raw_header": { + "title": "Raw Header", + "type": "string" + }, + "reply_to": { + "title": "Reply To", + "type": "string" + }, + "reply_to_list": { + "items": { + "type": "string" + }, + "title": "Reply To List", + "type": "array" + }, + "reply_to_mailboxes": { + "items": { + "type": "string" + }, + "title": "Reply To Mailboxes", + "type": "array" + }, + "return_path": { + "title": "Return Path", + "type": "string" + }, + "sender": { + "title": "Sender", + "type": "string" + }, + "sender_mailbox": { + "title": "Sender Mailbox", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "smtp_from": { + "title": "SMTP From", + "type": "string" + }, + "smtp_to": { + "items": { + "type": "string" + }, + "title": "SMTP To", + "type": "array" + }, + "subject": { + "title": "Subject", + "type": "string" + }, + "to": { + "items": { + "type": "string" + }, + "title": "To", + "type": "array" + }, + "to_mailboxes": { + "items": { + "type": "string" + }, + "title": "To Mailboxes", + "type": "array" + }, + "uid": { + "title": "Email Thread UID", + "type": "string" + }, + "urls": { + "items": { + "$ref": "#/$defs/url" + }, + "title": "URLs", + "type": "array" + }, + "x_originating_ip": { + "items": { + "type": "string" + }, + "title": "X-Originating-IP", + "type": "array" + } + }, + "title": "Email", + "type": "object" + }, + "environment_variable": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Environment Variable", + "type": "object" + }, + "technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Technique", + "type": "object" + }, + "http_request": { + "additionalProperties": false, + "properties": { + "args": { + "title": "HTTP Arguments", + "type": "string" + }, + "body_length": { + "title": "Request Body Length", + "type": "integer" + }, + "http_headers": { + "items": { + "$ref": "#/$defs/http_header" + }, + "title": "HTTP Headers", + "type": "array" + }, + "http_method": { + "enum": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "title": "HTTP Method", + "type": "string" + }, + "length": { + "title": "Request Length", + "type": "integer" + }, + "referrer": { + "title": "HTTP Referrer", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url": { + "$ref": "#/$defs/url", + "title": "URL" + }, + "user_agent": { + "title": "HTTP User-Agent", + "type": "string" + }, + "version": { + "title": "HTTP Version", + "type": "string" + }, + "x_forwarded_for": { + "items": { + "type": "string" + }, + "title": "X-Forwarded-For", + "type": "array" + } + }, + "title": "HTTP Request", + "type": "object" + }, + "cvss": { + "additionalProperties": false, + "properties": { + "base_score": { + "title": "Base Score", + "type": "number" + }, + "depth": { + "enum": [ + "Base", + "Environmental", + "Temporal" + ], + "title": "CVSS Depth", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "overall_score": { + "title": "Overall Score", + "type": "number" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "vector_string": { + "title": "Vector String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "base_score", + "version" + ], + "title": "CVSS Score", + "type": "object" + }, + "fingerprint": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "algorithm_id", + "value" + ], + "title": "Fingerprint", + "type": "object" + }, + "process_entity": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "cpid" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "path" + ] + }, + { + "required": [ + "pid" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "cpid": { + "title": "Common Process Identifier", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "pid": { + "title": "Process ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Process Entity", + "type": "object" + }, + "observable": { + "additionalProperties": false, + "properties": { + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Observable", + "type": "object" + }, + "http_header": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Header", + "type": "object" + }, + "attack": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "sub_technique" + ] + }, + { + "required": [ + "tactic" + ] + }, + { + "required": [ + "technique" + ] + } + ], + "properties": { + "mitigation": { + "$ref": "#/$defs/mitigation", + "title": "MITRE Mitigation" + }, + "sub_technique": { + "$ref": "#/$defs/sub_technique", + "title": "MITRE Sub-technique" + }, + "tactic": { + "$ref": "#/$defs/tactic", + "title": "MITRE Tactic" + }, + "tactics": { + "items": { + "$ref": "#/$defs/tactic" + }, + "title": "Tactics", + "type": "array" + }, + "technique": { + "$ref": "#/$defs/technique", + "title": "MITRE Technique" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "MITRE ATT&CK\u00ae & ATLAS\u2122", + "type": "object" + }, + "network_interface": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "mac" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "namespace": { + "title": "Namespace", + "type": "string" + }, + "open_ports": { + "items": { + "$ref": "#/$defs/port_info" + }, + "title": "Open Ports", + "type": "array" + }, + "subnet_prefix": { + "title": "Subnet Prefix Length", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Network Interface", + "type": "object" + }, + "user": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "account" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "account": { + "$ref": "#/$defs/account", + "title": "Account" + }, + "credential_uid": { + "title": "User Credential ID", + "type": "string" + }, + "display_name": { + "title": "Display Name", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "forward_addr": { + "title": "Forwarding Address", + "type": "string" + }, + "full_name": { + "title": "Full Name", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "has_mfa": { + "title": "MFA Assigned", + "type": "boolean" + }, + "ldap_person": { + "$ref": "#/$defs/ldap_person", + "title": "LDAP Person" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "phone_number": { + "title": "Telephone Number", + "type": "string" + }, + "programmatic_credentials": { + "items": { + "$ref": "#/$defs/programmatic_credential" + }, + "title": "Programmatic Credentials", + "type": "array" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "User", + "type": "object" + }, + "auth_factor": { + "additionalProperties": false, + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "email_addr": { + "title": "Email Address", + "type": "string" + }, + "factor_type": { + "title": "Factor Type", + "type": "string" + }, + "factor_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Factor Type ID", + "type": "integer" + }, + "is_hotp": { + "title": "HMAC-based One-time Password (HOTP)", + "type": "boolean" + }, + "is_totp": { + "title": "Time-based One-time Password (TOTP)", + "type": "boolean" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "security_questions": { + "items": { + "type": "string" + }, + "title": "Security Questions", + "type": "array" + } + }, + "required": [ + "factor_type_id" + ], + "title": "Authentication Factor", + "type": "object" + }, + "osint": { + "additionalProperties": false, + "properties": { + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "email_auth": { + "$ref": "#/$defs/email_auth", + "title": "Related Email Authentication" + }, + "external_uid": { + "title": "External ID", + "type": "string" + }, + "tlp": { + "enum": [ + "AMBER", + "AMBER STRICT", + "CLEAR", + "GREEN", + "RED", + "WHITE" + ], + "title": "Traffic Light Protocol", + "type": "string" + }, + "detection_pattern": { + "title": "Detection Pattern", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "category": { + "title": "Category", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "creator": { + "$ref": "#/$defs/user", + "title": "Creator" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Related Subdomains", + "type": "array" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "reputation": { + "$ref": "#/$defs/reputation", + "title": "Reputation Scores" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uploaded_time": { + "title": "Uploaded Time", + "type": "integer" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "comment": { + "title": "Analyst Comments", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "value": { + "title": "Indicator", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "intrusion_sets": { + "items": { + "type": "string" + }, + "title": "Intrusion Sets", + "type": "array" + }, + "script": { + "$ref": "#/$defs/script", + "title": "Related Script Data" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "related_analytics": { + "items": { + "$ref": "#/$defs/analytic" + }, + "title": "Related Analytics", + "type": "array" + }, + "file": { + "$ref": "#/$defs/file", + "title": "Related File" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "email": { + "$ref": "#/$defs/email", + "title": "Related Email" + }, + "kill_chain": { + "items": { + "$ref": "#/$defs/kill_chain_phase" + }, + "title": "Kill Chain", + "type": "array" + }, + "detection_pattern_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Detection Pattern Type ID", + "type": "integer" + }, + "detection_pattern_type": { + "title": "Detection Pattern", + "type": "string" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "threat_actor": { + "$ref": "#/$defs/threat_actor", + "title": "Threat Actor" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 99 + ], + "title": "Indicator Type ID", + "type": "integer" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "answers": { + "items": { + "$ref": "#/$defs/dns_answer" + }, + "title": "Related DNS Answers", + "type": "array" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "campaign": { + "$ref": "#/$defs/campaign", + "title": "Campaign" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "subnet": { + "title": "Related Subnet", + "type": "string" + }, + "signatures": { + "items": { + "$ref": "#/$defs/digital_signature" + }, + "title": "Related Digital Signatures", + "type": "array" + }, + "whois": { + "$ref": "#/$defs/whois", + "title": "WHOIS" + }, + "vulnerabilities": { + "items": { + "$ref": "#/$defs/vulnerability" + }, + "title": "Related Vulnerabilities", + "type": "array" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "OSINT", + "type": "object" + }, + "san": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + } + }, + "required": [ + "name", + "type" + ], + "title": "Subject Alternative Name", + "type": "object" + }, + "occurrence_details": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cell_name" + ] + }, + { + "required": [ + "column_name" + ] + }, + { + "required": [ + "column_number" + ] + }, + { + "required": [ + "end_line" + ] + }, + { + "required": [ + "json_path" + ] + }, + { + "required": [ + "page_number" + ] + }, + { + "required": [ + "record_index_in_array" + ] + }, + { + "required": [ + "row_number" + ] + }, + { + "required": [ + "start_line" + ] + } + ], + "properties": { + "cell_name": { + "title": "Cell Name", + "type": "string" + }, + "column_name": { + "title": "Column Name", + "type": "string" + }, + "column_number": { + "title": "Column Number", + "type": "integer" + }, + "end_line": { + "title": "End Line", + "type": "integer" + }, + "json_path": { + "title": "JSON Path", + "type": "string" + }, + "page_number": { + "title": "Page Number", + "type": "integer" + }, + "record_index_in_array": { + "title": "Record Index in Array", + "type": "integer" + }, + "row_number": { + "title": "Row Number", + "type": "integer" + }, + "start_line": { + "title": "Start Line", + "type": "integer" + } + }, + "title": "Occurrence Details", + "type": "object" + }, + "firewall_rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "condition": { + "title": "Condition", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "match_details": { + "items": { + "type": "string" + }, + "title": "Match Details", + "type": "array" + }, + "match_location": { + "title": "Match Location", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "rate_limit": { + "title": "Rate Limit", + "type": "integer" + }, + "sensitivity": { + "title": "Sensitivity", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Firewall Rule", + "type": "object" + }, + "threat_actor": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Threat Actor Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Threat Actor Type ID", + "type": "integer" + } + }, + "required": [ + "name" + ], + "title": "Threat Actor", + "type": "object" + }, + "package": { + "additionalProperties": false, + "properties": { + "architecture": { + "title": "Architecture", + "type": "string" + }, + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "epoch": { + "title": "Epoch", + "type": "integer" + }, + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "license": { + "title": "Software License", + "type": "string" + }, + "license_url": { + "title": "Software License URL", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "package_manager": { + "title": "Package Manager", + "type": "string" + }, + "package_manager_url": { + "title": "Package Manager URL", + "type": "string" + }, + "purl": { + "title": "Package URL", + "type": "string" + }, + "release": { + "title": "Software Release Details", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Package UID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name", + "version" + ], + "title": "Software Package", + "type": "object" + }, + "packet": { + "additionalProperties": false, + "properties": { + "encoding": { + "title": "Encoding", + "type": "string" + }, + "encoding_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Encoding ID", + "type": "integer" + }, + "end_offset": { + "title": "End Offset", + "type": "integer" + }, + "format": { + "title": "Format", + "type": "string" + }, + "format_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Format ID", + "type": "integer" + }, + "sequence_number": { + "title": "Sequence Number", + "type": "integer" + }, + "source": { + "title": "Source", + "type": "string" + }, + "source_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Source ID", + "type": "integer" + }, + "start_offset": { + "title": "Start Offset", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "value" + ], + "title": "Packet", + "type": "object" + }, + "transformation_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Transformation Info", + "type": "object" + }, + "network_connection_info": { + "additionalProperties": false, + "properties": { + "boundary": { + "title": "Boundary", + "type": "string" + }, + "boundary_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 99 + ], + "title": "Boundary ID", + "type": "integer" + }, + "community_uid": { + "title": "Community ID", + "type": "string" + }, + "direction": { + "title": "Direction", + "type": "string" + }, + "direction_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Direction ID", + "type": "integer" + }, + "flag_history": { + "title": "Connection Flag History", + "type": "string" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + }, + "protocol_ver": { + "title": "IP Version", + "type": "string" + }, + "protocol_ver_id": { + "enum": [ + 0, + 4, + 6, + 99 + ], + "title": "IP Version ID", + "type": "integer" + }, + "session": { + "$ref": "#/$defs/session", + "title": "Session" + }, + "tcp_flags": { + "title": "TCP Flags", + "type": "integer" + }, + "uid": { + "title": "Connection UID", + "type": "string" + } + }, + "required": [ + "direction_id" + ], + "title": "Network Connection Information", + "type": "object" + }, + "response": { + "additionalProperties": false, + "properties": { + "code": { + "title": "Response Code", + "type": "integer" + }, + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "error": { + "title": "Error Code", + "type": "string" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "message": { + "title": "Message", + "type": "string" + } + }, + "title": "Response Elements", + "type": "object" + }, + "reporter": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hostname": { + "title": "Hostname", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Reporter", + "type": "object" + }, + "load_balancer": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification": { + "title": "Classification", + "type": "string" + }, + "code": { + "title": "Response Code", + "type": "integer" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "endpoint_connections": { + "items": { + "$ref": "#/$defs/endpoint_connection" + }, + "title": "Endpoint Connections", + "type": "array" + }, + "error_message": { + "title": "Error Message", + "type": "string" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "message": { + "title": "Message", + "type": "string" + }, + "metrics": { + "items": { + "$ref": "#/$defs/metric" + }, + "title": "Metrics", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Load Balancer", + "type": "object" + }, + "url": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "path" + ] + }, + { + "required": [ + "url_string" + ] + } + ], + "properties": { + "categories": { + "items": { + "type": "string" + }, + "title": "Website Categorization", + "type": "array" + }, + "category_ids": { + "items": { + "enum": [ + 0, + 1, + 3, + 4, + 5, + 6, + 7, + 9, + 11, + 14, + 15, + 16, + 17, + 18, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 40, + 43, + 44, + 45, + 46, + 47, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 63, + 64, + 65, + 66, + 67, + 68, + 71, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 92, + 93, + 95, + 96, + 97, + 98, + 99, + 101, + 102, + 103, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 118, + 121 + ], + "type": "integer" + }, + "title": "Website Categorization IDs", + "type": "array" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "query_string": { + "title": "HTTP Query String", + "type": "string" + }, + "resource_type": { + "title": "Resource Type", + "type": "string" + }, + "scheme": { + "title": "Scheme", + "type": "string" + }, + "subdomain": { + "title": "Subdomain", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + } + }, + "title": "Uniform Resource Locator", + "type": "object" + }, + "object": { + "additionalProperties": true, + "properties": {}, + "title": "Object", + "type": "object" + }, + "extension": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Schema Extension", + "type": "object" + }, + "gpu_info": { + "additionalProperties": false, + "properties": { + "bus_type": { + "title": "Bus Type", + "type": "string" + }, + "bus_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Bus Type ID", + "type": "integer" + }, + "cores": { + "title": "Cores", + "type": "integer" + }, + "model": { + "title": "Model", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "vram_mode": { + "title": "VRAM Mode", + "type": "string" + }, + "vram_mode_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "VRAM Mode ID", + "type": "integer" + }, + "vram_size": { + "title": "VRAM Size", + "type": "integer" + } + }, + "title": "GPU Information", + "type": "object" + }, + "port_info": { + "additionalProperties": false, + "properties": { + "port": { + "title": "Port", + "type": "integer" + }, + "protocol_name": { + "title": "Protocol Name", + "type": "string" + }, + "protocol_num": { + "title": "Protocol Number", + "type": "integer" + } + }, + "required": [ + "port" + ], + "title": "Port Information", + "type": "object" + }, + "service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Service", + "type": "object" + }, + "tls": { + "additionalProperties": false, + "properties": { + "alert": { + "title": "Client TLS Alert", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "certificate_chain": { + "items": { + "type": "string" + }, + "title": "Certificate Chain", + "type": "array" + }, + "cipher": { + "title": "Cipher Suite", + "type": "string" + }, + "client_ciphers": { + "items": { + "type": "string" + }, + "title": "Client Cipher Suites", + "type": "array" + }, + "extension_list": { + "items": { + "$ref": "#/$defs/tls_extension" + }, + "title": "Extension List", + "type": "array" + }, + "handshake_dur": { + "title": "Handshake Duration", + "type": "integer" + }, + "ja3_hash": { + "$ref": "#/$defs/fingerprint", + "title": "JA3 Hash" + }, + "ja3s_hash": { + "$ref": "#/$defs/fingerprint", + "title": "JA3S Hash" + }, + "key_length": { + "title": "Key Length", + "type": "integer" + }, + "sans": { + "items": { + "$ref": "#/$defs/san" + }, + "title": "Subject Alternative Names", + "type": "array" + }, + "server_ciphers": { + "items": { + "type": "string" + }, + "title": "Server Cipher Suites", + "type": "array" + }, + "sni": { + "title": "Server Name Indication", + "type": "string" + }, + "tls_extension_list": { + "items": { + "$ref": "#/$defs/tls_extension" + }, + "title": "TLS Extension List", + "type": "array" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "version" + ], + "title": "Transport Layer Security (TLS)", + "type": "object" + }, + "metric": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "Metric", + "type": "object" + }, + "request": { + "additionalProperties": false, + "properties": { + "containers": { + "items": { + "$ref": "#/$defs/container" + }, + "title": "Containers", + "type": "array" + }, + "data": { + "title": "Data" + }, + "flags": { + "items": { + "type": "string" + }, + "title": "Flags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Request Elements", + "type": "object" + }, + "sub_technique": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE Sub-technique", + "type": "object" + }, + "container": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "hash": { + "$ref": "#/$defs/fingerprint", + "title": "Hash" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "network_driver": { + "title": "Network Driver", + "type": "string" + }, + "orchestrator": { + "title": "Orchestrator", + "type": "string" + }, + "pod_uuid": { + "title": "Pod UUID", + "type": "string" + }, + "runtime": { + "title": "Runtime", + "type": "string" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "tag": { + "title": "Image Tag", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Container", + "type": "object" + }, + "malware": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "classification_ids": { + "items": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 99 + ], + "type": "integer" + }, + "title": "Classification IDs", + "type": "array" + }, + "classifications": { + "items": { + "type": "string" + }, + "title": "Classifications", + "type": "array" + }, + "cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "CVE List", + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/file" + }, + "title": "Files", + "type": "array" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "path": { + "title": "Path", + "type": "string" + }, + "provider": { + "title": "Provider", + "type": "string" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "classification_ids" + ], + "title": "Malware", + "type": "object" + }, + "win_win_service": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "cmd_line" + ] + }, + { + "required": [ + "service_category_id" + ] + }, + { + "required": [ + "service_dependencies" + ] + }, + { + "required": [ + "service_error_control_id" + ] + }, + { + "required": [ + "service_start_name" + ] + }, + { + "required": [ + "service_start_type_id" + ] + }, + { + "required": [ + "service_type_id" + ] + } + ], + "properties": { + "cmd_line": { + "title": "Command Line", + "type": "string" + }, + "hosting_process": { + "$ref": "#/$defs/process_entity", + "title": "Hosting Process" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "load_order_group": { + "title": "Load Order Group", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "service_category": { + "title": "Service Category", + "type": "string" + }, + "service_category_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Service Category ID", + "type": "integer" + }, + "service_dependencies": { + "items": { + "type": "string" + }, + "title": "Service Dependencies", + "type": "array" + }, + "service_dll_file": { + "$ref": "#/$defs/file", + "title": "Service DLL" + }, + "service_error_control": { + "title": "Service Error Control", + "type": "string" + }, + "service_error_control_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Error Control ID", + "type": "integer" + }, + "service_file": { + "$ref": "#/$defs/file", + "title": "Service File" + }, + "service_start_name": { + "title": "Service Start Name", + "type": "string" + }, + "service_start_type": { + "title": "Service Start Type", + "type": "string" + }, + "service_start_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Service Start Type ID", + "type": "integer" + }, + "service_type": { + "title": "Service Type", + "type": "string" + }, + "service_type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Service Type ID", + "type": "integer" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Windows Service", + "type": "object" + }, + "group": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "desc": { + "title": "Description", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "privileges": { + "items": { + "type": "string" + }, + "title": "Privileges", + "type": "array" + }, + "type": { + "title": "Group Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + } + }, + "title": "Group", + "type": "object" + }, + "device": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "region": { + "title": "Region", + "type": "string" + }, + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "os_machine_uuid": { + "title": "OS Machine UUID", + "type": "string" + }, + "hypervisor": { + "title": "Hypervisor", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "is_backed_up": { + "title": "Back Ups Configured", + "type": "boolean" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "first_seen_time": { + "title": "First Seen", + "type": "integer" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "imei_list": { + "items": { + "type": "string" + }, + "title": "IMEI List", + "type": "array" + }, + "org": { + "$ref": "#/$defs/organization", + "title": "Organization" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "image": { + "$ref": "#/$defs/image", + "title": "Image" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "is_shared": { + "title": "Shared Device", + "type": "boolean" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "meid": { + "title": "MEID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "groups": { + "items": { + "$ref": "#/$defs/group" + }, + "title": "Groups", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "is_personal": { + "title": "Personal Device", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "iccid": { + "title": "ICCID", + "type": "string" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "uid_alt": { + "title": "Alternate ID", + "type": "string" + }, + "udid": { + "title": "Unique Device Identifier", + "type": "string" + }, + "model": { + "title": "Model", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "last_seen_time": { + "title": "Last Seen", + "type": "integer" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "boot_time": { + "title": "Boot Time", + "type": "integer" + }, + "is_supervised": { + "title": "Supervised Device", + "type": "boolean" + }, + "is_managed": { + "title": "Managed Device", + "type": "boolean" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "is_trusted": { + "title": "Trusted Device", + "type": "boolean" + }, + "boot_uid": { + "title": "Boot UID", + "type": "string" + }, + "network_interfaces": { + "items": { + "$ref": "#/$defs/network_interface" + }, + "title": "Network Interfaces", + "type": "array" + }, + "autoscale_uid": { + "title": "Autoscale UID", + "type": "string" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "subnet": { + "title": "Subnet", + "type": "string" + }, + "eid": { + "title": "EID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "imei": { + "title": "IMEI", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "is_mobile_account_active": { + "title": "Mobile Account Active", + "type": "boolean" + }, + "is_compliant": { + "title": "Compliant Device", + "type": "boolean" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Device", + "type": "object" + }, + "campaign": { + "additionalProperties": false, + "properties": { + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "title": "Campaign", + "type": "object" + }, + "cwe": { + "additionalProperties": false, + "properties": { + "caption": { + "title": "Caption", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "CWE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CWE", + "type": "object" + }, + "digital_signature": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Algorithm ID", + "type": "integer" + }, + "certificate": { + "$ref": "#/$defs/certificate", + "title": "Certificate" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "developer_uid": { + "title": "Developer UID", + "type": "string" + }, + "digest": { + "$ref": "#/$defs/fingerprint", + "title": "Message Digest" + }, + "state": { + "title": "State", + "type": "string" + }, + "state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 99 + ], + "title": "State ID", + "type": "integer" + } + }, + "required": [ + "algorithm_id" + ], + "title": "Digital Signature", + "type": "object" + }, + "malware_scan_info": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "end_time": { + "title": "End Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "num_files": { + "title": "Scanned Files", + "type": "integer" + }, + "num_infected": { + "title": "Number of Infected Entities", + "type": "integer" + }, + "num_volumes": { + "title": "Number of Volumes", + "type": "integer" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Scan UID", + "type": "string" + }, + "unique_malware_count": { + "title": "Unique Malware Count", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "Malware Scan Info", + "type": "object" + }, + "kill_chain_phase": { + "additionalProperties": false, + "properties": { + "phase": { + "title": "Kill Chain Phase", + "type": "string" + }, + "phase_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 99 + ], + "title": "Kill Chain Phase ID", + "type": "integer" + } + }, + "required": [ + "phase_id" + ], + "title": "Kill Chain Phase", + "type": "object" + }, + "organization": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "ou_name": { + "title": "Org Unit Name", + "type": "string" + }, + "ou_uid": { + "title": "Org Unit ID", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "Organization", + "type": "object" + }, + "encryption_details": { + "additionalProperties": false, + "properties": { + "algorithm": { + "title": "Encryption Algorithm", + "type": "string" + }, + "algorithm_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Encryption Algorithm ID", + "type": "integer" + }, + "key_length": { + "title": "Encryption Key Length", + "type": "integer" + }, + "key_uid": { + "title": "Key UID", + "type": "string" + }, + "type": { + "title": "Encryption Type", + "type": "string" + } + }, + "title": "Encryption Details", + "type": "object" + }, + "logger": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "event_uid": { + "title": "Event UID", + "type": "string" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "log_format": { + "title": "Log Format", + "type": "string" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Logger", + "type": "object" + }, + "feature": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Feature", + "type": "object" + }, + "ja4_fingerprint": { + "additionalProperties": false, + "properties": { + "section_a": { + "title": "JA4 Section A", + "type": "string" + }, + "section_b": { + "title": "JA4 Section B", + "type": "string" + }, + "section_c": { + "title": "JA4 Section C", + "type": "string" + }, + "section_d": { + "title": "JA4 Section D", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "type_id", + "value" + ], + "title": "JA4+ Fingerprint", + "type": "object" + }, + "timespan": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "duration" + ] + }, + { + "required": [ + "duration_days" + ] + }, + { + "required": [ + "duration_hours" + ] + }, + { + "required": [ + "duration_mins" + ] + }, + { + "required": [ + "duration_months" + ] + }, + { + "required": [ + "duration_secs" + ] + }, + { + "required": [ + "duration_weeks" + ] + }, + { + "required": [ + "duration_years" + ] + }, + { + "required": [ + "end_time" + ] + }, + { + "required": [ + "start_time" + ] + } + ], + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "duration_days": { + "title": "Duration Days", + "type": "integer" + }, + "duration_hours": { + "title": "Duration Hours", + "type": "integer" + }, + "duration_mins": { + "title": "Duration Minutes", + "type": "integer" + }, + "duration_months": { + "title": "Duration Months", + "type": "integer" + }, + "duration_secs": { + "title": "Duration Seconds", + "type": "integer" + }, + "duration_weeks": { + "title": "Duration Weeks", + "type": "integer" + }, + "duration_years": { + "title": "Duration Years", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "type": { + "title": "Time Span Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Time Span Type ID", + "type": "integer" + } + }, + "title": "Time Span", + "type": "object" + }, + "rule": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "category": { + "title": "Category", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Rule", + "type": "object" + }, + "http_cookie": { + "additionalProperties": false, + "properties": { + "domain": { + "title": "Domain", + "type": "string" + }, + "expiration_time": { + "title": "Expiration Time", + "type": "integer" + }, + "http_only": { + "title": "HTTP Only", + "type": "boolean" + }, + "is_http_only": { + "title": "HTTP Only", + "type": "boolean" + }, + "is_secure": { + "title": "Secure", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "samesite": { + "title": "SameSite", + "type": "string" + }, + "secure": { + "title": "Secure", + "type": "boolean" + }, + "value": { + "title": "Value", + "type": "string" + } + }, + "required": [ + "name", + "value" + ], + "title": "HTTP Cookie", + "type": "object" + }, + "agent": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Agent Name", + "type": "string" + }, + "policies": { + "items": { + "$ref": "#/$defs/policy" + }, + "title": "Agent Policies", + "type": "array" + }, + "type": { + "title": "Agent Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "uid": { + "title": "Agent ID", + "type": "string" + }, + "uid_alt": { + "title": "Alternate Agent ID", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Agent Version", + "type": "string" + } + }, + "title": "Agent", + "type": "object" + }, + "whois": { + "additionalProperties": false, + "properties": { + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "created_time": { + "title": "Registered At", + "type": "integer" + }, + "dnssec_status": { + "title": "DNSSEC Status", + "type": "string" + }, + "dnssec_status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "DNSSEC Status ID", + "type": "integer" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "domain_contacts": { + "items": { + "$ref": "#/$defs/domain_contact" + }, + "title": "Domain Contacts", + "type": "array" + }, + "email_addr": { + "title": "Registrar Abuse Email Address", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "last_seen_time": { + "title": "Last Updated At", + "type": "integer" + }, + "name_servers": { + "items": { + "type": "string" + }, + "title": "Name Servers", + "type": "array" + }, + "phone_number": { + "title": "Registrar Abuse Phone Number", + "type": "string" + }, + "registrar": { + "title": "Domain Registrar", + "type": "string" + }, + "status": { + "title": "Domain Status", + "type": "string" + }, + "subdomains": { + "items": { + "type": "string" + }, + "title": "Subdomains", + "type": "array" + }, + "subnet": { + "title": "Subnet Block", + "type": "string" + } + }, + "title": "WHOIS", + "type": "object" + }, + "key_value_object": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "value" + ] + }, + { + "required": [ + "values" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "value": { + "title": "Value", + "type": "string" + }, + "values": { + "items": { + "type": "string" + }, + "title": "Values", + "type": "array" + } + }, + "required": [ + "name" + ], + "title": "Key:Value object", + "type": "object" + }, + "metadata": { + "additionalProperties": false, + "properties": { + "log_format": { + "title": "Log Source Format", + "type": "string" + }, + "transformation_info_list": { + "items": { + "$ref": "#/$defs/transformation_info" + }, + "title": "Transformation Info", + "type": "array" + }, + "uid": { + "title": "Event UID", + "type": "string" + }, + "original_event_uid": { + "title": "Original Event ID", + "type": "string" + }, + "log_name": { + "title": "Log Name", + "type": "string" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "log_level": { + "title": "Log Level", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "log_version": { + "title": "Log Version", + "type": "string" + }, + "extensions": { + "items": { + "$ref": "#/$defs/extension" + }, + "title": "Schema Extensions", + "type": "array" + }, + "source": { + "title": "Source", + "type": "string" + }, + "untruncated_size": { + "title": "Untruncated Size", + "type": "integer" + }, + "loggers": { + "items": { + "$ref": "#/$defs/logger" + }, + "title": "Loggers", + "type": "array" + }, + "processed_time": { + "title": "Processed Time", + "type": "integer" + }, + "log_source": { + "title": "Log Source", + "type": "string" + }, + "logged_time": { + "title": "Logged Time", + "type": "integer" + }, + "debug": { + "items": { + "type": "string" + }, + "title": "Debug Information", + "type": "array" + }, + "profiles": { + "items": { + "type": "string" + }, + "title": "Profiles", + "type": "array" + }, + "log_provider": { + "title": "Log Provider", + "type": "string" + }, + "event_code": { + "title": "Event Code", + "type": "string" + }, + "tags": { + "items": { + "$ref": "#/$defs/key_value_object" + }, + "title": "Tags", + "type": "array" + }, + "tenant_uid": { + "title": "Tenant UID", + "type": "string" + }, + "reporter": { + "$ref": "#/$defs/reporter", + "title": "Reporter" + }, + "correlation_uid": { + "title": "Correlation UID", + "type": "string" + }, + "extension": { + "$ref": "#/$defs/extension", + "title": "Schema Extension" + }, + "labels": { + "items": { + "type": "string" + }, + "title": "Labels", + "type": "array" + }, + "sequence": { + "title": "Sequence Number", + "type": "integer" + }, + "transmit_time": { + "title": "Transmission Time", + "type": "integer" + }, + "is_truncated": { + "title": "Is Truncated", + "type": "boolean" + }, + "original_time": { + "title": "Original Time", + "type": "string" + }, + "total_queued_duration": { + "$ref": "#/$defs/timespan", + "title": "Total Queued Duration" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "required": [ + "product", + "version" + ], + "title": "Metadata", + "type": "object" + }, + "product": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "cpe_name": { + "title": "The product CPE identifier", + "type": "string" + }, + "feature": { + "$ref": "#/$defs/feature", + "title": "Feature" + }, + "lang": { + "title": "Language", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "path": { + "title": "Path", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "url_string": { + "title": "URL String", + "type": "string" + }, + "vendor_name": { + "title": "Vendor Name", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Product", + "type": "object" + }, + "advisory": { + "additionalProperties": false, + "properties": { + "avg_timespan": { + "$ref": "#/$defs/timespan", + "title": "Average Timespan" + }, + "bulletin": { + "title": "Patch Bulletin", + "type": "string" + }, + "classification": { + "title": "Classification", + "type": "string" + }, + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "install_state": { + "title": "Install State", + "type": "string" + }, + "install_state_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Install State ID", + "type": "integer" + }, + "is_superseded": { + "title": "The patch is superseded.", + "type": "boolean" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cves": { + "items": { + "$ref": "#/$defs/cve" + }, + "title": "Related CVEs", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "size": { + "title": "Size", + "type": "integer" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "title": { + "title": "Title", + "type": "string" + }, + "uid": { + "title": "Advisory ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "Advisory", + "type": "object" + }, + "network_endpoint": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "domain" + ] + }, + { + "required": [ + "hostname" + ] + }, + { + "required": [ + "instance_uid" + ] + }, + { + "required": [ + "interface_name" + ] + }, + { + "required": [ + "interface_uid" + ] + }, + { + "required": [ + "ip" + ] + }, + { + "required": [ + "name" + ] + }, + { + "required": [ + "svc_name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "interface_name": { + "title": "Network Interface Name", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "interface_uid": { + "title": "Network Interface ID", + "type": "string" + }, + "os": { + "$ref": "#/$defs/os", + "title": "OS" + }, + "port": { + "title": "Port", + "type": "integer" + }, + "type": { + "title": "Type", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Geo Location" + }, + "instance_uid": { + "title": "Instance ID", + "type": "string" + }, + "mac": { + "title": "MAC Address", + "type": "string" + }, + "autonomous_system": { + "$ref": "#/$defs/autonomous_system", + "title": "Autonomous System" + }, + "pool": { + "$ref": "#/$defs/group", + "title": "Pool" + }, + "subnet_uid": { + "title": "Subnet UID", + "type": "string" + }, + "isp": { + "title": "ISP Name", + "type": "string" + }, + "zone": { + "title": "Network Zone", + "type": "string" + }, + "name": { + "title": "Name", + "type": "string" + }, + "hw_info": { + "$ref": "#/$defs/device_hw_info", + "title": "Hardware Info" + }, + "namespace_pid": { + "title": "Namespace PID", + "type": "integer" + }, + "network_scope_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Network Scope ID", + "type": "integer" + }, + "svc_name": { + "title": "Service Name", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 99 + ], + "title": "Type ID", + "type": "integer" + }, + "network_scope": { + "title": "Network Scope", + "type": "string" + }, + "owner": { + "$ref": "#/$defs/user", + "title": "Owner" + }, + "container": { + "$ref": "#/$defs/container", + "title": "Container" + }, + "intermediate_ips": { + "items": { + "type": "string" + }, + "title": "Intermediate IP Addresses", + "type": "array" + }, + "isp_org": { + "title": "ISP Org", + "type": "string" + }, + "fingerprints": { + "items": { + "$ref": "#/$defs/fingerprint" + }, + "title": "Fingerprints", + "type": "array" + }, + "vpc_uid": { + "title": "VPC UID", + "type": "string" + }, + "domain": { + "title": "Domain", + "type": "string" + }, + "agent_list": { + "items": { + "$ref": "#/$defs/agent" + }, + "title": "Agent List", + "type": "array" + }, + "ip": { + "title": "IP Address", + "type": "string" + }, + "hostname": { + "title": "Hostname", + "type": "string" + }, + "vlan_uid": { + "title": "VLAN", + "type": "string" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "mac_vendor": { + "title": "MAC Vendor", + "type": "string" + } + }, + "title": "Network Endpoint", + "type": "object" + }, + "d3f_tactic": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "name": { + "title": "Name", + "type": "string" + }, + "src_url": { + "title": "Source URL", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "title": "MITRE D3FEND\u2122 Tactic", + "type": "object" + }, + "cve": { + "additionalProperties": false, + "properties": { + "created_time": { + "title": "Created Time", + "type": "integer" + }, + "cvss": { + "items": { + "$ref": "#/$defs/cvss" + }, + "title": "CVSS Score", + "type": "array" + }, + "cwe": { + "$ref": "#/$defs/cwe", + "title": "CWE" + }, + "cwe_uid": { + "title": "CWE UID", + "type": "string" + }, + "cwe_url": { + "title": "CWE URL", + "type": "string" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "epss": { + "$ref": "#/$defs/epss", + "title": "EPSS" + }, + "modified_time": { + "title": "Modified Time", + "type": "integer" + }, + "product": { + "$ref": "#/$defs/product", + "title": "Product" + }, + "references": { + "items": { + "type": "string" + }, + "title": "References", + "type": "array" + }, + "related_cwes": { + "items": { + "$ref": "#/$defs/cwe" + }, + "title": "Related CWEs", + "type": "array" + }, + "title": { + "title": "Title", + "type": "string" + }, + "type": { + "title": "Vulnerability Type", + "type": "string" + }, + "uid": { + "title": "CVE ID", + "type": "string" + } + }, + "required": [ + "uid" + ], + "title": "CVE", + "type": "object" + }, + "domain_contact": { + "additionalProperties": false, + "properties": { + "email_addr": { + "title": "Contact Email", + "type": "string" + }, + "location": { + "$ref": "#/$defs/location", + "title": "Contact Location Information" + }, + "name": { + "title": "Name", + "type": "string" + }, + "phone_number": { + "title": "Phone Number", + "type": "string" + }, + "type": { + "title": "Domain Contact Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 99 + ], + "title": "Domain Contact Type ID", + "type": "integer" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "type_id" + ], + "title": "Domain Contact", + "type": "object" + }, + "tls_extension": { + "additionalProperties": false, + "properties": { + "data": { + "title": "Data" + }, + "type": { + "title": "Type", + "type": "string" + }, + "type_id": { + "enum": [ + 0, + 1, + 5, + 10, + 13, + 14, + 15, + 16, + 18, + 19, + 20, + 21, + 41, + 42, + 43, + 44, + 45, + 47, + 48, + 49, + 50, + 51, + 99 + ], + "title": "Type ID", + "type": "integer" + } + }, + "required": [ + "type_id" + ], + "title": "TLS Extension", + "type": "object" + }, + "policy": { + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "name" + ] + }, + { + "required": [ + "type" + ] + }, + { + "required": [ + "uid" + ] + } + ], + "properties": { + "data": { + "title": "Data" + }, + "desc": { + "title": "Description", + "type": "string" + }, + "group": { + "$ref": "#/$defs/group", + "title": "Group" + }, + "is_applied": { + "title": "Applied", + "type": "boolean" + }, + "name": { + "title": "Name", + "type": "string" + }, + "type": { + "title": "Type", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + }, + "version": { + "title": "Version", + "type": "string" + } + }, + "title": "Policy", + "type": "object" + }, + "span": { + "additionalProperties": false, + "properties": { + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "message": { + "title": "Message", + "type": "string" + }, + "operation": { + "title": "Operation", + "type": "string" + }, + "parent_uid": { + "title": "Parent Unique ID", + "type": "string" + }, + "service": { + "$ref": "#/$defs/service", + "title": "Service" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "uid": { + "title": "Unique ID", + "type": "string" + } + }, + "required": [ + "end_time", + "start_time", + "uid" + ], + "title": "Span", + "type": "object" + } + }, + "$id": "https://schema.ocsf.io/schema/classes/http_activity", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "anyOf": [ + { + "required": [ + "http_request" + ] + }, + { + "required": [ + "http_response" + ] + } + ], + "properties": { + "traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Traffic" + }, + "unmapped": { + "$ref": "#/$defs/object", + "title": "Unmapped Data" + }, + "message": { + "title": "Message", + "type": "string" + }, + "http_response": { + "$ref": "#/$defs/http_response", + "title": "HTTP Response" + }, + "app_name": { + "title": "Application Name", + "type": "string" + }, + "disposition": { + "title": "Disposition", + "type": "string" + }, + "proxy_http_request": { + "$ref": "#/$defs/http_request", + "title": "Proxy HTTP Request" + }, + "app_protocol_name": { + "title": "Application Protocol Name", + "type": "string" + }, + "proxy_connection_info": { + "$ref": "#/$defs/network_connection_info", + "title": "Proxy Connection Info" + }, + "category_uid": { + "const": 4, + "title": "Category ID", + "type": "integer" + }, + "action": { + "title": "Action", + "type": "string" + }, + "confidence": { + "title": "Confidence", + "type": "string" + }, + "ja4_fingerprint_list": { + "items": { + "$ref": "#/$defs/ja4_fingerprint" + }, + "title": "JA4+ Fingerprints", + "type": "array" + }, + "cumulative_traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Cumulative Traffic" + }, + "risk_details": { + "title": "Risk Details", + "type": "string" + }, + "duration": { + "title": "Duration Milliseconds", + "type": "integer" + }, + "status_detail": { + "title": "Status Detail", + "type": "string" + }, + "confidence_score": { + "title": "Confidence Score", + "type": "integer" + }, + "proxy_tls": { + "$ref": "#/$defs/tls", + "title": "Proxy TLS" + }, + "status_code": { + "title": "Status Code", + "type": "string" + }, + "disposition_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26, + 27, + 99 + ], + "title": "Disposition ID", + "type": "integer" + }, + "type_name": { + "title": "Type Name", + "type": "string" + }, + "network_observation_point": { + "$ref": "#/$defs/network_endpoint", + "title": "Network Observation Point" + }, + "risk_level": { + "title": "Risk Level", + "type": "string" + }, + "raw_data": { + "title": "Raw Data", + "type": "string" + }, + "time": { + "title": "Event Time", + "type": "integer" + }, + "status_id": { + "enum": [ + 0, + 1, + 2, + 99 + ], + "title": "Status ID", + "type": "integer" + }, + "actor": { + "$ref": "#/$defs/actor", + "title": "Actor" + }, + "proxy_http_response": { + "$ref": "#/$defs/http_response", + "title": "Proxy HTTP Response" + }, + "trace": { + "$ref": "#/$defs/trace", + "title": "Trace" + }, + "attacks": { + "items": { + "$ref": "#/$defs/attack" + }, + "title": "MITRE ATT&CK\u00ae and ATLAS\u2122 Details", + "type": "array" + }, + "status": { + "title": "Status", + "type": "string" + }, + "activity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 99 + ], + "title": "Activity ID", + "type": "integer" + }, + "file": { + "$ref": "#/$defs/file", + "title": "File" + }, + "packet_list": { + "items": { + "$ref": "#/$defs/packet" + }, + "title": "Packets", + "type": "array" + }, + "risk_score": { + "title": "Risk Score", + "type": "integer" + }, + "proxy_traffic": { + "$ref": "#/$defs/network_traffic", + "title": "Proxy Traffic" + }, + "observables": { + "items": { + "$ref": "#/$defs/observable" + }, + "title": "Observables", + "type": "array" + }, + "enrichments": { + "items": { + "$ref": "#/$defs/enrichment" + }, + "title": "Enrichments", + "type": "array" + }, + "http_request": { + "$ref": "#/$defs/http_request", + "title": "HTTP Request" + }, + "authorizations": { + "items": { + "$ref": "#/$defs/authorization" + }, + "title": "Authorization Information", + "type": "array" + }, + "raw_data_hash": { + "$ref": "#/$defs/fingerprint", + "title": "Raw Data Hash" + }, + "action_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Action ID", + "type": "integer" + }, + "confidence_id": { + "enum": [ + 0, + 1, + 2, + 3, + 99 + ], + "title": "Confidence ID", + "type": "integer" + }, + "observation_point_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Observation Point ID", + "type": "integer" + }, + "firewall_rule": { + "$ref": "#/$defs/firewall_rule", + "title": "Firewall Rule" + }, + "proxy": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy" + }, + "class_uid": { + "const": 4002, + "title": "Class ID", + "type": "integer" + }, + "http_status": { + "title": "HTTP Status", + "type": "integer" + }, + "activity_name": { + "title": "Activity", + "type": "string" + }, + "tls": { + "$ref": "#/$defs/tls", + "title": "TLS" + }, + "start_time": { + "title": "Start Time", + "type": "integer" + }, + "class_name": { + "title": "Class", + "type": "string" + }, + "timezone_offset": { + "title": "Timezone Offset", + "type": "integer" + }, + "risk_level_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 99 + ], + "title": "Risk Level ID", + "type": "integer" + }, + "raw_data_size": { + "title": "Raw Data Size", + "type": "integer" + }, + "malware": { + "items": { + "$ref": "#/$defs/malware" + }, + "title": "Malware", + "type": "array" + }, + "observation_point": { + "title": "Observation Point", + "type": "string" + }, + "dst_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Destination Endpoint" + }, + "is_alert": { + "title": "Alert", + "type": "boolean" + }, + "device": { + "$ref": "#/$defs/device", + "title": "Device" + }, + "severity": { + "title": "Severity", + "type": "string" + }, + "category_name": { + "title": "Category", + "type": "string" + }, + "malware_scan_info": { + "$ref": "#/$defs/malware_scan_info", + "title": "Malware Scan Info" + }, + "severity_id": { + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 99 + ], + "title": "Severity ID", + "type": "integer" + }, + "count": { + "title": "Count", + "type": "integer" + }, + "http_cookies": { + "items": { + "$ref": "#/$defs/http_cookie" + }, + "title": "HTTP Cookies", + "type": "array" + }, + "end_time": { + "title": "End Time", + "type": "integer" + }, + "type_uid": { + "title": "Type ID", + "type": "integer" + }, + "src_endpoint": { + "$ref": "#/$defs/network_endpoint", + "title": "Source Endpoint" + }, + "metadata": { + "$ref": "#/$defs/metadata", + "title": "Metadata" + }, + "proxy_endpoint": { + "$ref": "#/$defs/network_proxy", + "title": "Proxy Endpoint" + }, + "connection_info": { + "$ref": "#/$defs/network_connection_info", + "title": "Connection Info" + }, + "policy": { + "$ref": "#/$defs/policy", + "title": "Policy" + } + }, + "required": [ + "activity_id", + "category_uid", + "class_uid", + "metadata", + "severity_id", + "time", + "type_uid" + ], + "title": "HTTP Activity", + "type": "object" +} diff --git a/crates/openshell-ocsf/src/events/base_event.rs b/crates/openshell-ocsf/src/events/base_event.rs index e023516de5..4e8554f7a8 100644 --- a/crates/openshell-ocsf/src/events/base_event.rs +++ b/crates/openshell-ocsf/src/events/base_event.rs @@ -13,6 +13,10 @@ use crate::objects::{AiModel, Device, Metadata}; /// Every event class embeds this struct via `#[serde(flatten)]`. #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] pub struct BaseEventData { + /// Trace correlation (Trace profile), written only through `set_trace`. + #[serde(default)] + pub(crate) trace: Option, + /// OCSF class UID (e.g., 4001 for Network Activity). pub class_uid: u32, @@ -76,7 +80,6 @@ impl Serialize for BaseEventData { fn serialize(&self, serializer: S) -> Result { use serde::ser::SerializeMap; - // Count fields: 9 required + severity pair (2) + up to 6 optional let mut map = serializer.serialize_map(None)?; map.serialize_entry("class_uid", &self.class_uid)?; @@ -112,6 +115,11 @@ impl Serialize for BaseEventData { if let Some(ref ai_model) = self.ai_model { map.serialize_entry("ai_model", ai_model)?; } + if let Some(ref trace) = self.trace + && crate::trace::accepts_trace(self.class_uid) + { + map.serialize_entry("trace", trace)?; + } if let Some(ref unmapped) = self.unmapped { map.serialize_entry("unmapped", unmapped)?; } @@ -154,6 +162,7 @@ impl BaseEventData { metadata, device: None, ai_model: None, + trace: None, unmapped: None, } } @@ -188,6 +197,25 @@ impl BaseEventData { self.ai_model = Some(ai_model); } + /// Set trace correlation and add the Trace profile once. + /// + /// OCSF 1.8.0 declares the Trace profile only for HTTP Activity and API + /// Activity, so other classes ignore it. + pub(crate) fn set_trace(&mut self, trace: crate::TraceCorrelation) { + if !crate::trace::accepts_trace(self.class_uid) { + return; + } + self.trace = Some(trace); + if !self + .metadata + .profiles + .iter() + .any(|profile| profile == "trace") + { + self.metadata.profiles.push("trace".to_string()); + } + } + /// Add an unmapped field. pub fn add_unmapped(&mut self, key: &str, value: impl Into) { let map = self diff --git a/crates/openshell-ocsf/src/events/mod.rs b/crates/openshell-ocsf/src/events/mod.rs index e57a870f54..66038cd9cb 100644 --- a/crates/openshell-ocsf/src/events/mod.rs +++ b/crates/openshell-ocsf/src/events/mod.rs @@ -168,6 +168,22 @@ impl OcsfEvent { Self::Base(e) => &e.base, } } + + /// Returns mutable base data common to all event classes. + pub(crate) fn base_mut(&mut self) -> &mut BaseEventData { + match self { + Self::NetworkActivity(e) => &mut e.base, + Self::HttpActivity(e) => &mut e.base, + Self::SshActivity(e) => &mut e.base, + Self::ProcessActivity(e) => &mut e.base, + Self::Authentication(e) => &mut e.base, + Self::DetectionFinding(e) => &mut e.base, + Self::ApplicationLifecycle(e) => &mut e.base, + Self::DeviceConfigStateChange(e) => &mut e.base, + Self::ApiActivity(e) => &mut e.base, + Self::Base(e) => &mut e.base, + } + } } #[cfg(test)] diff --git a/crates/openshell-ocsf/src/lib.rs b/crates/openshell-ocsf/src/lib.rs index 661b4c6f5a..4b169e165c 100644 --- a/crates/openshell-ocsf/src/lib.rs +++ b/crates/openshell-ocsf/src/lib.rs @@ -31,6 +31,9 @@ pub mod events; pub mod format; pub mod objects; pub mod sanitize; +mod trace; + +pub use trace::{TraceCorrelation, register_trace_correlation_extractor}; pub mod tracing_layers; #[cfg(any(test, feature = "test-support"))] diff --git a/crates/openshell-ocsf/src/trace.rs b/crates/openshell-ocsf/src/trace.rs new file mode 100644 index 0000000000..9651e00336 --- /dev/null +++ b/crates/openshell-ocsf/src/trace.rs @@ -0,0 +1,226 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OCSF trace correlation and optional enrichment at emission. + +use std::sync::OnceLock; + +use serde::{Deserialize, Serialize}; + +/// W3C trace ID of the span active when an OCSF event was emitted. +/// +/// This is a correlation hint, not an identity assertion: gateway context may +/// inherit a caller's traceparent, including before authentication. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "TraceFields")] +pub struct TraceCorrelation { + uid: String, +} + +#[derive(Deserialize)] +struct TraceFields { + uid: String, +} + +impl TryFrom for TraceCorrelation { + type Error = &'static str; + + fn try_from(fields: TraceFields) -> Result { + let uid = fields.uid; + let hex = uid.len() == 32 && uid.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')); + if !hex || uid.bytes().all(|b| b == b'0') { + return Err("trace.uid must be a non-zero W3C trace ID in lowercase hex"); + } + Ok(Self { uid }) + } +} + +impl TraceCorrelation { + /// Returns `None` for the invalid all-zero trace ID. + #[must_use] + pub fn from_trace_id(trace_id: [u8; 16]) -> Option { + if trace_id == [0; 16] { + return None; + } + Some(Self { + uid: format!("{:032x}", u128::from_be_bytes(trace_id)), + }) + } + + /// The trace ID as 32 lowercase hexadecimal characters. + #[must_use] + pub fn uid(&self) -> &str { + &self.uid + } +} + +static EXTRACTOR: OnceLock Option> = OnceLock::new(); + +/// Register the process-wide trace extractor at startup. +/// +/// Returns `false` if an extractor is already registered. Without an extractor, +/// events carry no trace correlation. +pub fn register_trace_correlation_extractor(extractor: fn() -> Option) -> bool { + EXTRACTOR.set(extractor).is_ok() +} + +/// HTTP Activity and API Activity: the classes OCSF 1.8.0 declares the Trace +/// profile for. +pub const fn accepts_trace(class_uid: u32) -> bool { + matches!(class_uid, 4002 | 6003) +} + +pub fn enrich(event: &mut crate::OcsfEvent) { + if accepts_trace(event.class_uid()) + && event.base().trace.is_none() + && let Some(trace) = EXTRACTOR.get().and_then(|extract| extract()) + { + event.base_mut().set_trace(trace); + } +} + +#[cfg(test)] +mod tests { + use crate::*; + use openshell_ocsf_validate::validate_event; + + const TRACE_ID: [u8; 16] = [ + 0x0a, 0xf7, 0x65, 0x19, 0x16, 0xcd, 0x43, 0xdd, 0x84, 0x48, 0xeb, 0x21, 0x1c, 0x80, 0x31, + 0x9c, + ]; + + fn trace() -> TraceCorrelation { + TraceCorrelation::from_trace_id(TRACE_ID).unwrap() + } + + #[test] + fn trace_id_renders_as_lowercase_w3c_hex() { + assert_eq!(trace().uid(), "0af7651916cd43dd8448eb211c80319c"); + assert!(TraceCorrelation::from_trace_id([0; 16]).is_none()); + } + + #[test] + fn deserialization_rejects_malformed_trace_ids() { + let valid = serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c"}); + assert_eq!( + serde_json::from_value::(valid).unwrap(), + trace() + ); + for uid in [ + "", + "0AF7651916CD43DD8448EB211C80319C", + "0af7651916cd43dd8448eb211c80319", + "0af7651916cd43dd8448eb211c80319cz", + "00000000000000000000000000000000", + ] { + assert!( + serde_json::from_value::(serde_json::json!({ "uid": uid })) + .is_err(), + "{uid:?} accepted" + ); + } + } + + #[test] + fn production_event_shapes_validate_with_and_without_trace() { + let ctx = builders::test_sandbox_context(); + for (class, mut event) in builders::sample_events(&ctx) { + let plain = event.to_json().unwrap(); + validate_event(&plain).unwrap_or_else(|e| panic!("{class} without trace: {e}")); + event.base_mut().set_trace(trace()); + let correlated = event.to_json().unwrap(); + validate_event(&correlated).unwrap_or_else(|e| panic!("{class} with trace: {e}")); + if matches!(class, "http_activity" | "api_activity") { + assert_eq!(correlated["trace"]["uid"], trace().uid(), "{class}"); + } else { + assert_eq!(correlated, plain, "{class}"); + } + let decoded: OcsfEvent = serde_json::from_value(correlated.clone()).unwrap(); + assert_eq!(decoded.to_json().unwrap(), correlated); + } + } + + #[test] + fn deserialized_trace_is_dropped_from_classes_without_the_profile() { + let ctx = builders::test_sandbox_context(); + let (_, event) = builders::sample_events(&ctx) + .into_iter() + .find(|(class, _)| *class == "network_activity") + .unwrap(); + let mut value = event.to_json().unwrap(); + value["trace"] = serde_json::json!({"uid": trace().uid()}); + let decoded: OcsfEvent = serde_json::from_value(value).unwrap(); + assert!(decoded.to_json().unwrap().get("trace").is_none()); + } + + #[test] + fn gateway_api_activity_shapes_validate_with_trace() { + let ctx = builders::test_gateway_context(); + let events = [ + ApiActivityBuilder::new(&ctx, "CreateSandbox").build(), + ApiActivityBuilder::new(&ctx, "CreateSandbox") + .action(ActionId::Denied) + .build(), + ApiActivityBuilder::new(&ctx, "chat.completions") + .ai_model(AiModel::new("model", "provider")) + .build(), + ]; + for mut event in events { + event.base_mut().set_trace(trace()); + let correlated = event.to_json().unwrap(); + validate_event(&correlated).unwrap_or_else(|e| panic!("{correlated}: {e}")); + assert_eq!(correlated["trace"]["uid"], trace().uid()); + } + } + + #[test] + fn exported_schema_checks_trace_shape_and_declared_profile() { + let ctx = builders::test_sandbox_context(); + let (_, mut event) = builders::sample_events(&ctx) + .into_iter() + .find(|(class, _)| *class == "http_activity") + .unwrap(); + event.base_mut().set_trace(trace()); + let mut value = event.to_json().unwrap(); + validate_event(&value).unwrap_or_else(|e| panic!("{e}")); + value["trace"]["uid"] = serde_json::json!(42); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"] = serde_json::json!({}); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"] = + serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c", "bogus": true}); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + value["trace"].as_object_mut().unwrap().remove("bogus"); + value["metadata"]["profiles"] + .as_array_mut() + .unwrap() + .retain(|name| name != "trace"); + assert!(matches!( + validate_event(&value), + Err(openshell_ocsf_validate::Error::Invalid(_)) + )); + } + + #[test] + fn plain_event_omits_trace_profile_and_object() { + let ctx = builders::test_sandbox_context(); + let value = BaseEventBuilder::new(&ctx).build().to_json().unwrap(); + assert!(value.get("trace").is_none()); + assert!( + !value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); + } +} diff --git a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs index c4a5ccb895..ca5a5e9cdd 100644 --- a/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs +++ b/crates/openshell-ocsf/src/tracing_layers/event_bridge.rs @@ -34,7 +34,8 @@ pub fn clone_current_event() -> Option { /// as human-readable shorthand and structured JSONL for their configured sinks. /// /// Both layers receive the event — `clone_current_event()` is non-consuming. -pub fn emit_ocsf_event(event: OcsfEvent) { +pub fn emit_ocsf_event(mut event: OcsfEvent) { + crate::trace::enrich(&mut event); // Store the event in thread-local so layers can access it set_current_event(event); @@ -81,7 +82,8 @@ pub fn clear_current_event() { /// bare `emit_ocsf_event`, but the gateway hosts many sandboxes, so it stamps a /// per-event `sandbox_id` field here instead. One tracing event feeds both the /// JSONL audit file and the routing bus. -pub fn emit_ocsf_event_routed(sandbox_id: &str, event: OcsfEvent) { +pub fn emit_ocsf_event_routed(sandbox_id: &str, mut event: OcsfEvent) { + crate::trace::enrich(&mut event); let message = event.format_shorthand(); set_current_event(event); tracing::info!(target: "ocsf", sandbox_id = %sandbox_id, message = %message); diff --git a/crates/openshell-otel/src/correlation.rs b/crates/openshell-otel/src/correlation.rs new file mode 100644 index 0000000000..d451ef725b --- /dev/null +++ b/crates/openshell-otel/src/correlation.rs @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Sampled trace correlation for OCSF producers. + +use opentelemetry::trace::{SpanContext, TraceContextExt as _}; + +/// Extract the active span's W3C trace ID for an OCSF event. +/// +/// Returns `None` unless the current span has valid, sampled context. This reads +/// the current span without extracting workload HTTP headers or environment +/// variables. The active span may already inherit a gateway caller's context, +/// including before authentication. This is a correlation hint, not proof of +/// origin. A sampled ID does not guarantee export or retention. +#[must_use] +pub fn current_ocsf_trace_correlation() -> Option { + let span_context = sampled_span_context()?; + openshell_ocsf::TraceCorrelation::from_trace_id(span_context.trace_id().to_bytes()) +} + +/// The OpenTelemetry tracing layer activates a span's context on entry, so this +/// also works inside a layer, where the tracing dispatcher is busy. +pub fn sampled_span_context() -> Option { + let context = opentelemetry::Context::current(); + let span_context = context.span().span_context().clone(); + (span_context.is_valid() && span_context.is_sampled()).then_some(span_context) +} diff --git a/crates/openshell-otel/src/lib.rs b/crates/openshell-otel/src/lib.rs index 78b995ee0d..1f9de6bf17 100644 --- a/crates/openshell-otel/src/lib.rs +++ b/crates/openshell-otel/src/lib.rs @@ -3,6 +3,9 @@ //! Shared OpenTelemetry trace and log export support for `OpenShell` services. +mod correlation; +pub use correlation::current_ocsf_trace_correlation; + mod driver; mod grpc; mod headers; @@ -54,6 +57,18 @@ use tracing_subscriber::Layer as _; use tracing_subscriber::layer::{Context, Filter}; use tracing_subscriber::registry::LookupSpan; +/// Export supervisor spans at INFO by default, or DEBUG and TRACE when enabled. +/// Dependency spans remain at INFO. +#[must_use] +pub fn supervisor_span_filter(log_level: &str) -> tracing_subscriber::EnvFilter { + use tracing_subscriber::filter::LevelFilter; + let level = log_level + .parse::() + .unwrap_or(LevelFilter::INFO) + .max(LevelFilter::INFO); + tracing_subscriber::EnvFilter::new(format!("info,openshell={level}")) +} + const SDK_UNKNOWN_SERVICE_PREFIX: &str = "unknown_service"; /// Build the resource attributes shared by gateway and compute-driver providers. @@ -566,6 +581,15 @@ pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { mod tests { use super::*; + #[test] + fn supervisor_span_filter_raises_only_openshell_targets() { + let directives = |log_level: &str| supervisor_span_filter(log_level).to_string(); + assert_eq!(directives("warn"), "openshell=info,info"); + assert_eq!(directives("info"), "openshell=info,info"); + assert_eq!(directives("debug"), "openshell=debug,info"); + assert_eq!(directives("info,h2=debug"), "openshell=info,info"); + } + #[test] fn error_status_guard_marks_only_unfinished_results() { use tracing_subscriber::layer::SubscriberExt as _; diff --git a/crates/openshell-otel/src/ocsf.rs b/crates/openshell-otel/src/ocsf.rs index 3ad4dd7173..63489fde1d 100644 --- a/crates/openshell-otel/src/ocsf.rs +++ b/crates/openshell-otel/src/ocsf.rs @@ -116,10 +116,11 @@ impl Layer for OcsfOtlpLayer { let Some(event) = clone_current_event() else { return; }; - let Ok(record) = log_record(&self.logger, &event, SystemTime::now()) else { + let Ok(mut record) = log_record(&self.logger, &event, SystemTime::now()) else { self.gap.record_loss("serialization", 1); return; }; + set_trace_context(&mut record); let bytes = record_bytes(&record); if bytes > BATCH_MAX_BYTES { self.gap.record_loss("oversized", 1); @@ -399,6 +400,18 @@ fn log_record( Ok(record) } +/// Link the record to the active sampled span through the OTLP record's own +/// trace fields, for every OCSF class. +fn set_trace_context(record: &mut SdkLogRecord) { + if let Some(span_context) = crate::correlation::sampled_span_context() { + record.set_trace_context( + span_context.trace_id(), + span_context.span_id(), + Some(span_context.trace_flags()), + ); + } +} + /// Map an OCSF severity onto an OTLP severity. /// /// OCSF ranks more levels than OTLP has bands. Low and Medium share the WARN @@ -418,7 +431,10 @@ fn severity(severity: SeverityId) -> Severity { #[cfg(test)] mod tests { use super::*; - use openshell_ocsf::{ConfigStateChangeBuilder, EventContext, EventOrigin, ocsf_emit}; + use openshell_ocsf::{ + ConfigStateChangeBuilder, EventContext, EventOrigin, HttpActivityBuilder, HttpRequest, Url, + ocsf_emit, + }; use opentelemetry::Key; use opentelemetry::logs::{AnyValue, Severity}; use opentelemetry_sdk::Resource; @@ -479,6 +495,41 @@ mod tests { assert_eq!(body(&record(&event)), native(&event)); } + #[test] + fn trace_body_preserves_native_correlation() { + let mut correlated = native( + &HttpActivityBuilder::new(&event_context("sandbox-1")) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "example.com", "/", 443), + )) + .build(), + ); + correlated["trace"] = serde_json::json!({"uid": "0af7651916cd43dd8448eb211c80319c"}); + correlated["metadata"]["profiles"] + .as_array_mut() + .unwrap() + .push("trace".into()); + let event: OcsfEvent = serde_json::from_value(correlated).unwrap(); + let native_body = body(&record(&event)); + assert_eq!(native_body, native(&event)); + assert_eq!( + native_body["trace"]["uid"], + "0af7651916cd43dd8448eb211c80319c" + ); + assert!( + native_body["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|profile| profile == "trace") + ); + assert_eq!( + native_body["metadata"]["version"], + openshell_ocsf::OCSF_VERSION + ); + } + #[test] fn timestamps_are_event_time_and_observed_time() { let event = event("sandbox-1", SeverityId::Informational); @@ -719,6 +770,42 @@ mod tests { assert_eq!(exporter.service_name().as_deref(), Some("openshell-test")); } + #[tokio::test] + async fn records_carry_the_sampled_span_context_for_every_class() { + use opentelemetry::trace::TraceContextExt as _; + use opentelemetry_sdk::trace::{InMemorySpanExporter, Sampler, SdkTracerProvider}; + use tracing_opentelemetry::OpenTelemetrySpanExt as _; + + for sampler in [Sampler::AlwaysOn, Sampler::AlwaysOff] { + let exporter = RecordingExporter::default(); + let otlp = OcsfOtlp::start(exporter.clone(), exporter_resource()); + let tracer = SdkTracerProvider::builder() + .with_sampler(sampler) + .with_simple_exporter(InMemorySpanExporter::default()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(crate::layer(&tracer, "ocsf-test")) + .with(otlp.layer()); + let span_context = tracing::subscriber::with_default(subscriber, || { + let span = tracing::info_span!("decision"); + span.in_scope(|| ocsf_emit!(event("sandbox-1", SeverityId::Medium))); + span.context().span().span_context().clone() + }); + otlp.shutdown().await; + + let records = exporter.exported(); + let trace = records[0].record.trace_context(); + if span_context.is_sampled() { + let trace = trace.expect("sampled span context is attached"); + assert_eq!(trace.trace_id, span_context.trace_id()); + assert_eq!(trace.span_id, span_context.span_id()); + assert_eq!(trace.trace_flags, Some(span_context.trace_flags())); + } else { + assert!(trace.is_none()); + } + } + } + #[tokio::test] async fn shutdown_exports_every_queued_record_in_bounded_batches() { let exporter = RecordingExporter::default(); diff --git a/crates/openshell-otel/tests/ocsf_correlation.rs b/crates/openshell-otel/tests/ocsf_correlation.rs new file mode 100644 index 0000000000..acf3f1a334 --- /dev/null +++ b/crates/openshell-otel/tests/ocsf_correlation.rs @@ -0,0 +1,228 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, LazyLock, Mutex}; + +use openshell_ocsf::{ + ApiActivityBuilder, AuthenticationBuilder, Endpoint, HttpActivityBuilder, HttpRequest, + NetworkActivityBuilder, OcsfEvent, OcsfJsonlLayer, Service, Url, User, ocsf_emit, +}; +use opentelemetry_sdk::trace::{InMemorySpanExporterBuilder, Sampler, SdkTracerProvider}; +use tracing_subscriber::prelude::*; + +static SETUP: LazyLock<()> = LazyLock::new(|| { + tracing::subscriber::set_global_default(tracing_subscriber::registry()).unwrap(); + assert!(openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + )); +}); + +#[derive(Clone)] +struct Writer(Arc>>); +impl std::io::Write for Writer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +fn capture( + sampler: Sampler, + active: bool, +) -> (serde_json::Value, Vec) { + capture_route(sampler, active, false) +} + +fn http_event() -> OcsfEvent { + HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .http_request(HttpRequest::new( + "GET", + Url::new("https", "example.com", "/", 443), + )) + .build() +} + +fn capture_event(build: fn() -> OcsfEvent) -> serde_json::Value { + capture_context(Sampler::AlwaysOn, true, false, None, build).0 +} + +fn capture_route( + sampler: Sampler, + active: bool, + routed: bool, +) -> (serde_json::Value, Vec) { + capture_context(sampler, active, routed, None, http_event) +} + +fn capture_context( + sampler: Sampler, + active: bool, + routed: bool, + remote_parent: Option, + build: fn() -> OcsfEvent, +) -> (serde_json::Value, Vec) { + LazyLock::force(&SETUP); + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_sampler(sampler) + .with_simple_exporter(exporter.clone()) + .build(); + let bytes = Arc::new(Mutex::new(vec![])); + let subscriber = tracing_subscriber::registry() + .with(openshell_otel::layer(&provider, "ocsf-test")) + .with(OcsfJsonlLayer::new(Writer(bytes.clone()))); + tracing::subscriber::with_default(subscriber, || { + let emit = || { + if routed { + openshell_ocsf::emit_ocsf_event_routed("test-sandbox", build()); + } else { + ocsf_emit!(build()); + } + }; + if active { + let span = tracing::info_span!("sampled-operation"); + if let Some(parent) = remote_parent { + use opentelemetry::trace::TraceContextExt as _; + use tracing_opentelemetry::OpenTelemetrySpanExt as _; + span.set_parent(opentelemetry::Context::new().with_remote_span_context(parent)) + .unwrap(); + } + span.in_scope(emit); + } else { + emit(); + } + }); + let value = serde_json::from_slice(&bytes.lock().unwrap()).unwrap(); + provider.force_flush().unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + provider.shutdown().unwrap(); + (value, spans) +} + +#[test] +fn api_activity_correlation_can_inherit_sampled_or_unsampled_caller_context() { + use opentelemetry::trace::{SpanContext, SpanId, TraceFlags, TraceId, TraceState}; + let uid = "0af7651916cd43dd8448eb211c80319c"; + for sampled in [true, false] { + let parent = SpanContext::new( + TraceId::from_hex(uid).unwrap(), + SpanId::from_hex("b7ad6b7169203331").unwrap(), + if sampled { + TraceFlags::SAMPLED + } else { + TraceFlags::default() + }, + true, + TraceState::default(), + ); + let (event, spans) = capture_context( + Sampler::ParentBased(Box::new(Sampler::AlwaysOn)), + true, + false, + Some(parent), + || ApiActivityBuilder::new(openshell_ocsf::ctx::ctx(), "CreateSandbox").build(), + ); + assert_eq!(event["class_uid"], 6003); + if sampled { + assert_eq!(event["trace"]["uid"], uid); + assert_eq!(spans.len(), 1); + assert_eq!(spans[0].span_context.trace_id().to_string(), uid); + } else { + assert_no_trace(&event); + assert!(spans.is_empty()); + } + } +} + +fn assert_no_trace(value: &serde_json::Value) { + assert!(value.get("trace").is_none()); + assert!( + !value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); +} + +#[test] +fn classes_without_the_trace_profile_stay_uncorrelated() { + assert_no_trace(&capture_event(|| { + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .dst_endpoint(Endpoint::from_domain("example.com", 443)) + .build() + })); + assert_no_trace(&capture_event(|| { + AuthenticationBuilder::new( + openshell_ocsf::ctx::ctx(), + User::unidentified(), + Service::new("openshell-gateway"), + ) + .build() + })); +} + +#[test] +fn sampled_active_span_correlates_to_exported_w3c_trace() { + let (value, spans) = capture(Sampler::AlwaysOn, true); + let uid = value["trace"]["uid"].as_str().unwrap(); + assert_eq!(uid.len(), 32); + assert!( + uid.bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + ); + assert_eq!(uid, spans[0].span_context.trace_id().to_string()); + assert_eq!( + value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .filter(|p| *p == "trace") + .count(), + 1 + ); + assert_eq!(value["trace"].as_object().unwrap().len(), 1); +} + +#[test] +fn no_active_context_omits_trace_and_profile() { + let (value, spans) = capture(Sampler::AlwaysOn, false); + assert_no_trace(&value); + assert!(spans.is_empty()); +} + +#[test] +fn routed_emission_also_enriches_from_sampled_context() { + let (value, spans) = capture_route(Sampler::AlwaysOn, true, true); + assert_eq!( + value["trace"]["uid"], + spans[0].span_context.trace_id().to_string() + ); + assert!( + value["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); +} + +#[test] +fn unsampled_active_context_omits_trace_and_profile() { + let (value, spans) = capture(Sampler::AlwaysOff, true); + assert_no_trace(&value); + assert!(spans.is_empty()); +} + +#[test] +fn active_tracing_span_without_otel_context_is_invalid() { + LazyLock::force(&SETUP); + tracing::subscriber::with_default(tracing_subscriber::registry(), || { + tracing::info_span!("no-otel-layer").in_scope(|| { + assert!(openshell_otel::current_ocsf_trace_correlation().is_none()); + }); + }); +} diff --git a/crates/openshell-server/src/cli.rs b/crates/openshell-server/src/cli.rs index 1f0f215881..55ad3091ea 100644 --- a/crates/openshell-server/src/cli.rs +++ b/crates/openshell-server/src/cli.rs @@ -799,6 +799,10 @@ async fn run_from_args( let prepared = prepare_server_config_with_drivers(&mut args, &matches, &compute_drivers)?; validate_user_auth(&prepared.config)?; + openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + ); + // Initialize OCSF identity before tracing can emit gateway events. let gateway_identity = crate::gateway_ocsf::GatewayIdentity { name: prepared.config.name.clone(), diff --git a/crates/openshell-supervisor-network/Cargo.toml b/crates/openshell-supervisor-network/Cargo.toml index aa3ca35014..7043f30d24 100644 --- a/crates/openshell-supervisor-network/Cargo.toml +++ b/crates/openshell-supervisor-network/Cargo.toml @@ -67,10 +67,12 @@ bundled-ca-roots = ["dep:webpki-roots"] openshell-sandbox-backend = { path = "../openshell-sandbox-backend", features = ["test-support"] } openshell-driver-vm = { path = "../openshell-driver-vm", default-features = false, features = ["allocation-tracking"] } +openshell-otel-test-support = { path = "../openshell-otel-test-support" } openshell-ocsf = { path = "../openshell-ocsf", features = ["test-support"] } openshell-ocsf-validate = { path = "../openshell-ocsf-validate" } openshell-otel = { path = "../openshell-otel" } openshell-supervisor-middleware-builtins = { path = "../openshell-supervisor-middleware-builtins" } +opentelemetry-proto = { workspace = true } opentelemetry_sdk = { workspace = true, features = ["testing"] } temp-env = "0.3" tokio = { workspace = true, features = ["test-util"] } diff --git a/crates/openshell-supervisor-network/clippy.toml b/crates/openshell-supervisor-network/clippy.toml new file mode 100644 index 0000000000..0b7005afb4 --- /dev/null +++ b/crates/openshell-supervisor-network/clippy.toml @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +disallowed-macros = [ + { path = "openshell_ocsf::ocsf_emit", reason = "use crate::telemetry::egress_emit so denied HTTP decisions get a deny span" }, +] diff --git a/crates/openshell-supervisor-network/src/google_cloud_metadata.rs b/crates/openshell-supervisor-network/src/google_cloud_metadata.rs index aec80473a3..ea5d0c16bd 100644 --- a/crates/openshell-supervisor-network/src/google_cloud_metadata.rs +++ b/crates/openshell-supervisor-network/src/google_cloud_metadata.rs @@ -16,8 +16,10 @@ use http::StatusCode; use miette::{IntoDiagnostic, Result}; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_ocsf::{ - ActivityId, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, ocsf_emit, + ActionId, ActivityId, DispositionId, HttpActivityBuilder, HttpRequest, SeverityId, StatusId, }; + +use crate::telemetry::egress_emit; use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; type MetadataResponse = (u16, &'static str, String); @@ -311,7 +313,7 @@ fn emit_metadata_event( status: StatusId, message: &str, ) { - ocsf_emit!(build_metadata_event( + egress_emit!(build_metadata_event( method, response_code, severity, @@ -327,7 +329,7 @@ fn build_metadata_event( status: StatusId, message: &str, ) -> openshell_ocsf::OcsfEvent { - HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + let mut builder = HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::for_http_method(method)) .http_request(HttpRequest { http_method: method.parse().expect("HTTP method parsing is infallible"), @@ -338,8 +340,13 @@ fn build_metadata_event( }) .severity(severity) .status(status) - .message(message.to_string()) - .build() + .message(message.to_string()); + if matches!(response_code, 403 | 405) { + builder = builder + .action(ActionId::Denied) + .disposition(DispositionId::Blocked); + } + builder.build() } #[cfg(test)] @@ -347,6 +354,26 @@ mod tests { use super::*; use std::collections::HashMap; + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn metadata_rejections_have_deny_traces_but_operational_failures_do_not() { + use crate::telemetry::tests::{assert_deny_resolves, exported}; + + let (events, received) = exported("warn", || { + let ctx = make_context(HashMap::new()); + route_request(&ctx, "GET", PATH_TOKEN, &[]); + route_request(&ctx, "POST", PATH_TOKEN, &flavor_headers()); + route_request(&ctx, "GET", "/unknown", &flavor_headers()); + route_request(&ctx, "GET", PATH_TOKEN, &flavor_headers()); + }) + .await; + assert_eq!(events.len(), 4); + assert_deny_resolves(&events[..2], &received, true); + for event in &events[2..] { + assert!(event.get("trace").is_none()); + } + assert_eq!(received.spans.len(), 2); + } + fn token_binding() -> openshell_core::proto::StaticCredentialBinding { openshell_core::proto::StaticCredentialBinding { endpoints: vec![openshell_core::proto::StaticCredentialEndpointBinding { diff --git a/crates/openshell-supervisor-network/src/host.rs b/crates/openshell-supervisor-network/src/host.rs index b5ada8b100..d6fbe2e54f 100644 --- a/crates/openshell-supervisor-network/src/host.rs +++ b/crates/openshell-supervisor-network/src/host.rs @@ -23,7 +23,7 @@ use openshell_core::proto::SandboxPolicy as ProtoSandboxPolicy; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_isolation_interface::contract::{BinaryIdentity, ExecutableIdentity, Sha256Digest}; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, }; use tokio::sync::mpsc::UnboundedSender; @@ -125,7 +125,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -138,7 +138,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -152,7 +152,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -167,7 +167,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -181,7 +181,7 @@ pub async fn start_host_proxy(config: HostProxyConfig) -> Result { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) diff --git a/crates/openshell-supervisor-network/src/l7/middleware.rs b/crates/openshell-supervisor-network/src/l7/middleware.rs index de2aa20613..73c317be6f 100644 --- a/crates/openshell-supervisor-network/src/l7/middleware.rs +++ b/crates/openshell-supervisor-network/src/l7/middleware.rs @@ -5,10 +5,11 @@ use crate::l7::relay::L7EvalContext; use crate::opa::PolicyGenerationGuard; +use crate::telemetry::egress_emit; use miette::{Result, miette}; use openshell_ocsf::{ ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo, - HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, ocsf_emit, + HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, }; use std::path::PathBuf; use tokio::io::{AsyncRead, AsyncWrite}; @@ -146,7 +147,7 @@ pub fn uninspectable_traffic_gate( /// Emit the detection finding for traffic a matching middleware chain cannot /// inspect: denied under a fail-closed chain, bypassed under fail-open. pub fn emit_middleware_uninspectable(ctx: &L7EvalContext, detail: &str, denied: bool) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(if denied { SeverityId::High } else { @@ -166,9 +167,11 @@ pub fn emit_middleware_uninspectable(ctx: &L7EvalContext, detail: &str, denied: "Uninspectable traffic to host with required middleware; denied" } else { "Uninspectable traffic bypassed middleware (fail_open)" - }) - .build(); - ocsf_emit!(event); + }); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } pub fn emit_websocket_preflight_events( @@ -178,13 +181,13 @@ pub fn emit_websocket_preflight_events( emit_websocket_invocations(ctx, &outcome.invocations); emit_websocket_coverage(ctx, &outcome.coverage); for event in websocket_preflight_finding_events(outcome) { - ocsf_emit!(event); + egress_emit!(event); } if outcome.saturated { emit_websocket_saturation(ctx); } if outcome.session_capacity_exhausted { - emit_middleware_session_capacity_exhausted(ctx); + emit_middleware_session_capacity_exhausted(ctx, !outcome.allowed); } } @@ -193,7 +196,7 @@ pub(super) fn emit_websocket_coverage( coverage: &[openshell_supervisor_middleware::WebSocketCoverage], ) { for event in websocket_coverage_events(ctx, coverage) { - ocsf_emit!(event); + egress_emit!(event); } } @@ -263,7 +266,7 @@ pub(super) fn emit_websocket_message_events( emit_websocket_saturation(ctx); } for event in websocket_message_finding_events(outcome) { - ocsf_emit!(event); + egress_emit!(event); } } @@ -385,10 +388,15 @@ fn emit_websocket_invocations( invocation.transformed, )) .build(); - ocsf_emit!(event); + egress_emit!(event); if invocation.failed { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) - .severity(if invocation.outcome == Outcome::FailClosed { + let fail_closed = invocation.outcome == Outcome::FailClosed; + let mut finding = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()); + if fail_closed { + finding = finding.action(ActionId::Denied); + } + let event = finding + .severity(if fail_closed { SeverityId::High } else { SeverityId::Medium @@ -406,7 +414,7 @@ fn emit_websocket_invocations( ]) .message("WebSocket middleware stage failed") .build(); - ocsf_emit!(event); + egress_emit!(event); } if invocation.stage_disabled { let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) @@ -426,7 +434,7 @@ fn emit_websocket_invocations( "WebSocket middleware stage stream became unusable and was disabled for this session", ) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } @@ -445,11 +453,11 @@ fn emit_websocket_saturation(ctx: &L7EvalContext) { ]) .message("WebSocket middleware work waited for admission capacity") .build(); - ocsf_emit!(event); + egress_emit!(event); } -fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) +fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext, denied: bool) { + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Medium) .finding_info(FindingInfo::new( "openshell.middleware.session_capacity_exhausted", @@ -460,13 +468,16 @@ fn emit_middleware_session_capacity_exhausted(ctx: &L7EvalContext) { ("host", ctx.host.as_str()), ("operation", "websocket_message"), ]) - .message("Persistent middleware session admission was refused at process capacity") - .build(); - ocsf_emit!(event); + .message("Persistent middleware session admission was refused at process capacity"); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } fn middleware_admission_exhausted_event(ctx: &L7EvalContext) -> openshell_ocsf::OcsfEvent { DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(ActionId::Denied) .severity(SeverityId::Medium) .finding_info(FindingInfo::new( "openshell.middleware.admission_exhausted", @@ -483,7 +494,7 @@ fn middleware_admission_exhausted_event(ctx: &L7EvalContext) -> openshell_ocsf:: } fn emit_middleware_admission_exhausted(ctx: &L7EvalContext) { - ocsf_emit!(middleware_admission_exhausted_event(ctx)); + egress_emit!(middleware_admission_exhausted_event(ctx)); } /// Largest body-buffering limit across the entries that actually resolved to a @@ -790,7 +801,7 @@ pub(super) fn resolve_unbuffered_body( } fn emit_middleware_body_unavailable(ctx: &L7EvalContext, denied: bool) { - let event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + let mut event = DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) .severity(if denied { SeverityId::High } else { @@ -809,9 +820,11 @@ fn emit_middleware_body_unavailable(ctx: &L7EvalContext, denied: bool) { "Request body exceeded middleware inspection cap; denied" } else { "Request body exceeded middleware inspection cap; passed through (fail_open)" - }) - .build(); - ocsf_emit!(event); + }); + if denied { + event = event.action(ActionId::Denied); + } + egress_emit!(event.build()); } /// Parse the raw header block into middleware-visible headers, preserving @@ -962,6 +975,7 @@ pub(super) fn middleware_events( "openshell.middleware.failure", "Supervisor middleware failure", )) + .action(ActionId::Denied) .message("Required supervisor middleware failed closed") .build(); events.push(event); @@ -981,6 +995,7 @@ pub(super) fn middleware_events( ("policy", ctx.policy_name.as_str()), ("host", ctx.host.as_str()), ]) + .action(ActionId::Denied) .message("Transformed request denied because policy evaluation failed") .build(); events.push(event); @@ -999,8 +1014,208 @@ fn emit_middleware_events( req: &crate::l7::provider::L7Request, outcome: &openshell_supervisor_middleware::ChainOutcome, ) { - for event in middleware_events(ctx, req, outcome) { - ocsf_emit!(event); + crate::telemetry::emit_decision(middleware_events(ctx, req, outcome), None); +} + +#[cfg(test)] +mod trace_tests { + use super::*; + use crate::telemetry::tests::{assert_deny_resolves, assert_no_deny, deny_spans, exported}; + use openshell_supervisor_middleware::{ChainOutcome, MiddlewareInvocation, NamespacedFinding}; + + fn finding() -> NamespacedFinding { + NamespacedFinding { + middleware: "guard".to_string(), + finding: openshell_core::proto::Finding { + r#type: "test.finding".to_string(), + label: "Test finding".to_string(), + count: 1, + severity: "high".to_string(), + ..Default::default() + }, + } + } + + fn outcome( + allowed: bool, + reason: &str, + applied: Vec, + findings: Vec, + ) -> ChainOutcome { + ChainOutcome { + allowed, + reason: reason.to_string(), + body: vec![], + header_mutations: vec![], + findings, + metadata: std::collections::BTreeMap::new(), + applied, + denial: None, + } + } + + fn request() -> crate::l7::provider::L7Request { + crate::l7::provider::L7Request { + action: "POST".to_string(), + target: "/".to_string(), + query_params: std::collections::HashMap::new(), + raw_header: vec![], + body_length: crate::l7::provider::BodyLength::None, + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn websocket_fail_closed_findings_record_the_denial() { + use openshell_supervisor_middleware::{ + WebSocketInvocation, WebSocketInvocationOutcome as Outcome, + }; + for (outcome, action_id) in [(Outcome::FailClosed, Some(2)), (Outcome::FailOpen, None)] { + let (events, _) = exported("warn", || { + emit_websocket_invocations( + &L7EvalContext::default(), + &[WebSocketInvocation { + config_name: "guard".to_string(), + implementation: "test/guard".to_string(), + outcome, + sequence: None, + original_size: 0, + replacement_size: None, + transformed: false, + failed: true, + stage_disabled: false, + reason_code: None, + }], + ); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.websocket_failure" + }) + .unwrap(); + assert_eq!(finding["action_id"].as_u64(), action_id, "{outcome:?}"); + } + } + + #[test] + fn refusal_findings_record_the_denial() { + for reason in [ + "middleware_failed: guard", + "transformed_body_policy_evaluation_failed: unavailable", + ] { + let events = middleware_events( + &L7EvalContext::default(), + &request(), + &outcome(false, reason, vec![], vec![]), + ); + assert_eq!(events.len(), 1, "{reason}"); + let finding = events[0].to_json().unwrap(); + assert_eq!(finding["class_uid"], 2004); + assert_eq!(finding["action_id"], 2, "{reason}"); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn inspection_refusals_record_denial_but_fail_open_findings_do_not() { + for denied in [false, true] { + let (events, received) = exported("warn", || { + let ctx = L7EvalContext::default(); + emit_middleware_uninspectable(&ctx, "unknown protocol", denied); + emit_middleware_body_unavailable(&ctx, denied); + }) + .await; + assert_eq!(events.len(), 2); + for (event, uid) in events.iter().zip([ + "openshell.middleware.traffic_uninspectable", + "openshell.middleware.body_unavailable", + ]) { + assert_eq!(event["finding_info"]["uid"], uid); + assert_eq!(event["class_uid"], 2004); + assert_eq!(event["action_id"].as_u64(), denied.then_some(2)); + assert_eq!(event["action"].as_str(), denied.then_some("Denied")); + if !denied { + assert!(event.get("action_id").is_none()); + assert!(event.get("action").is_none()); + } + } + // A finding-only refusal still creates no HTTP deny span. + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn session_capacity_findings_follow_the_preflight_outcome() { + for allowed in [true, false] { + let (events, received) = exported("warn", || { + emit_websocket_preflight_events( + &L7EvalContext::default(), + &openshell_supervisor_middleware::WebSocketPreflightResult { + allowed, + terminal_reason: None, + reason: "middleware_session_capacity_exhausted".into(), + denial: None, + session: None, + findings: vec![], + metadata: std::collections::BTreeMap::new(), + invocations: vec![], + coverage: vec![], + saturated: false, + session_capacity_exhausted: true, + }, + ); + }) + .await; + assert_eq!(events.len(), 1); + assert_eq!( + events[0]["finding_info"]["uid"], + "openshell.middleware.session_capacity_exhausted" + ); + assert_eq!(events[0]["action_id"].as_u64(), (!allowed).then_some(2)); + assert_eq!(events[0]["action"].as_str(), (!allowed).then_some("Denied")); + if allowed { + assert!(events[0].get("action_id").is_none()); + assert!(events[0].get("action").is_none()); + } + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn http_chain_outcomes_group_findings_and_preserve_allowed_controls() { + for allowed in [true, false] { + let (events, received) = exported("warn", || { + let invocation = MiddlewareInvocation { + name: "guard".to_string(), + implementation: "test/guard".to_string(), + decision: if allowed { + openshell_core::proto::Decision::Allow + } else { + openshell_core::proto::Decision::Deny + }, + transformed: false, + failed: false, + }; + let reason = if allowed { + "allowed" + } else { + "middleware_failed: guard" + }; + emit_middleware_events( + &L7EvalContext::default(), + &request(), + &outcome(allowed, reason, vec![invocation], vec![finding()]), + ); + }) + .await; + assert!(events.len() >= 2); + if allowed { + assert_no_deny(&events, &received); + } else { + assert_eq!(deny_spans(&received).len(), 1); + assert_deny_resolves(&events, &received, true); + } + } } } @@ -1026,6 +1241,10 @@ mod tests { let serialized = serde_json::to_string(&middleware_admission_exhausted_event(&ctx)) .expect("serialize admission event"); + let event: serde_json::Value = serde_json::from_str(&serialized).unwrap(); + assert_eq!(event["class_uid"], 2004); + assert_eq!(event["action_id"], 2); + assert_eq!(event["action"], "Denied"); assert!(serialized.contains("openshell.middleware.admission_exhausted")); assert!(serialized.contains("api-policy")); assert!(serialized.contains("api.example.test")); diff --git a/crates/openshell-supervisor-network/src/l7/mod.rs b/crates/openshell-supervisor-network/src/l7/mod.rs index 37849b9c0c..bc01d0bbdd 100644 --- a/crates/openshell-supervisor-network/src/l7/mod.rs +++ b/crates/openshell-supervisor-network/src/l7/mod.rs @@ -417,7 +417,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { "rejecting endpoint: unrecognized credential_signing value {other:?}" )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); return None; } _ => CredentialSigning::None, @@ -433,7 +433,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { .state(openshell_ocsf::StateId::Disabled, "invalid") .message("rejecting endpoint: credential_signing requires signing_service".to_string()) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); return None; } @@ -462,6 +462,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { pub(crate) fn emit_uninspected_credential_finding(host: &str, policy_name: &str, surface: &str) { let event = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.credentials.traffic_uninspectable", @@ -475,7 +476,7 @@ pub(crate) fn emit_uninspected_credential_finding(host: &str, policy_name: &str, ]) .message("Uninspected credential-bearing traffic denied") .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); } impl L7EndpointConfig { diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index b4acb5e52f..fa20252714 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -20,6 +20,7 @@ use crate::l7::provider::{L7Provider, RelayOutcome}; use crate::l7::rest::WebSocketExtensionMode; use crate::l7::{EndpointObserver, EnforcementMode, L7EndpointConfig, L7Protocol, L7RequestInfo}; use crate::opa::{PolicyGenerationGuard, TunnelPolicyEngine}; +use crate::telemetry::egress_emit; use miette::{IntoDiagnostic, Result, miette}; use openshell_core::activity::{ActivitySender, try_record_activity}; use openshell_core::endpoint_status::{EndpointObservationSender, EndpointResult}; @@ -27,7 +28,6 @@ use openshell_core::secrets::{self, SecretResolver}; use openshell_ocsf::{ ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo, HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, Url as OcsfUrl, - ocsf_emit, }; #[cfg(test)] use std::collections::BTreeMap; @@ -287,8 +287,13 @@ where .into_diagnostic()?; client.flush().await.into_diagnostic()?; - ocsf_emit!(build_request_authority_mismatch_event(ctx, method)); - ocsf_emit!(build_request_authority_mismatch_finding(ctx)); + crate::telemetry::emit_decision( + [ + build_request_authority_mismatch_event(ctx, method), + build_request_authority_mismatch_finding(ctx), + ], + None, + ); Ok(()) } @@ -338,7 +343,7 @@ where crate::l7::mcp::selected_revision_context(request, version) ); let summary = l7_protocol_log_summary(None, Some(&info)); - ocsf_emit!(build_l7_request_event( + egress_emit!(build_l7_request_event( ctx, &request.action, redacted_target, @@ -419,7 +424,7 @@ where } let reason = error.rejection_detail(request); let summary = l7_protocol_log_summary(None, Some(info)); - ocsf_emit!(build_l7_request_event( + egress_emit!(build_l7_request_event( ctx, &request.action, redacted_target, @@ -616,8 +621,8 @@ where .into_diagnostic()?; client.flush().await.into_diagnostic()?; - if placement_mismatch { - ocsf_emit!( + let mut events = if placement_mismatch { + vec![ HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::for_http_method(method)) .http_request(method_only_request(method)) @@ -630,24 +635,24 @@ where .firewall_rule(&ctx.policy_name, "credential-binding") .message("Credential use denied: unauthorized request location") .status_detail("credential_placement_mismatch") - .build() - ); - ocsf_emit!(crate::l7::build_credential_placement_mismatch_finding( - &ctx.policy_name, - &ctx.host, - None, - )); + .build(), + crate::l7::build_credential_placement_mismatch_finding( + &ctx.policy_name, + &ctx.host, + None, + ), + ] } else { - ocsf_emit!(build_credential_resolution_event( + vec![build_credential_resolution_event( ctx, method, - endpoint_mismatch - )); - } - + endpoint_mismatch, + )] + }; if endpoint_mismatch { - ocsf_emit!(build_credential_endpoint_mismatch_finding(ctx)); + events.push(build_credential_endpoint_mismatch_finding(ctx)); } + crate::telemetry::emit_decision(events, None); Ok(()) } @@ -1041,7 +1046,7 @@ fn emit_parse_rejection(ctx: &L7EvalContext, detail: &str, engine_type: &str) { )) .status_detail(detail) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity(ctx, true, "l7_parse_rejection"); } @@ -1157,7 +1162,7 @@ where .dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port)) .message("SQL L7 provider not yet implemented, falling back to passthrough") .build(); - ocsf_emit!(event); + egress_emit!(event); } tokio::io::copy_bidirectional(client, upstream) .await @@ -1690,7 +1695,7 @@ fn emit_l7_request_log( reason, protocol_summary, ); - ocsf_emit!(event); + egress_emit!(event); emit_activity(ctx, decision_str == "deny", "l7_policy"); } @@ -1856,7 +1861,7 @@ where } else { "raw bidirectional relay (L7 enforcement no longer active)" }; - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Other) .activity_name("Upgrade") @@ -2199,7 +2204,7 @@ where request_info.action, ctx.host, ctx.port, redacted_target, reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || config.enforcement == EnforcementMode::Audit { @@ -2407,7 +2412,7 @@ pub(crate) fn emit_policy_reload( port: u16, policy_name: &str, ) { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2627,7 +2632,7 @@ where &reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || (config.enforcement == EnforcementMode::Audit && !force_deny) { @@ -2909,7 +2914,7 @@ where request_info.action, ctx.host, ctx.port, redacted_target, reason, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } if allowed || (config.enforcement == EnforcementMode::Audit && !force_deny) { @@ -3380,7 +3385,7 @@ fn emit_transformed_body_decision( request_info.action, ctx.host, ctx.port, request_info.target, reason )) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn jsonrpc_policy_input(info: &crate::l7::jsonrpc::JsonRpcRequestInfo) -> serde_json::Value { @@ -3613,7 +3618,7 @@ where req.action, ctx.host, ctx.port, redacted_target, )) .build(); - ocsf_emit!(event); + egress_emit!(event); } let request_id = uuid::Uuid::new_v4().to_string(); @@ -3785,6 +3790,67 @@ where Ok(()) } +#[cfg(test)] +mod trace_tests { + use super::*; + use crate::telemetry::tests::{assert_deny_resolves, exported}; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn authority_mismatch_domain_and_finding_share_one_trace() { + let (events, received) = exported("warn", || { + let ctx = L7EvalContext { + host: "blocked.example".to_string(), + port: 443, + ..Default::default() + }; + let mut response = Vec::new(); + // The in-memory span processor exports with a futures executor. + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(reject_request_authority_mismatch( + &mut response, + &ctx, + "POST", + )) + }) + .unwrap(); + assert!(response.starts_with(b"HTTP/1.1 403 Forbidden")); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn default_level_l7_relay_deny_uid_resolves_to_one_span_root_trace() { + let (events, received) = crate::telemetry::tests::exported_over_otlp("warn", || { + let ctx = L7EvalContext { + host: "blocked.example".to_string(), + port: 443, + policy_name: "rest-policy".to_string(), + ..Default::default() + }; + egress_emit!(build_l7_request_event( + &ctx, "GET", "/", "deny", "l7-rest", "denied", None + )); + }) + .await; + assert_eq!(events.len(), 1); + assert_deny_resolves(&events, &received, true); + assert_eq!(received.spans.len(), 1); + crate::telemetry::tests::assert_deny_attribute( + &received, + "openshell.policy.name", + "rest-policy", + ); + crate::telemetry::tests::assert_deny_attribute( + &received, + "server.address", + "blocked.example", + ); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/openshell-supervisor-network/src/l7/rest.rs b/crates/openshell-supervisor-network/src/l7/rest.rs index 7a6a0d6236..97c71f09b3 100644 --- a/crates/openshell-supervisor-network/src/l7/rest.rs +++ b/crates/openshell-supervisor-network/src/l7/rest.rs @@ -1184,7 +1184,7 @@ where port = options.port, )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); } _ => { return Err(miette::Report::new(CredentialUnavailableError::new( @@ -1534,8 +1534,9 @@ fn emit_uninspected_body_credential_denial( req.action, options.host, options.port )) .build(); - openshell_ocsf::ocsf_emit!(event); + crate::telemetry::egress_emit!(event); let finding = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.credentials.traffic_uninspectable", @@ -1548,7 +1549,7 @@ fn emit_uninspected_body_credential_denial( ]) .message("Request body credential placeholder denied") .build(); - openshell_ocsf::ocsf_emit!(finding); + crate::telemetry::egress_emit!(finding); } struct PreparedRequestBody { @@ -6916,6 +6917,256 @@ mod tests { (outcome, delivered) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_failure_records_share_the_terminal_decision() { + use crate::telemetry::tests::{assert_deny_resolves, deny_spans, exported}; + + // This relay fixture has no CONNECT span, so denies are roots at both + // levels. The thread-local export harness observes the relay task; + // emissions in spawned tasks would need explicit subscriber propagation. + for level in ["warn", "debug"] { + for (method, script) in [ + ("GET", ResponseRelayScript::BlockStream), + ("GET", ResponseRelayScript::InvalidBodySequence), + ("GET", ResponseRelayScript::InvalidWholeBodySequence), + ("HEAD", ResponseRelayScript::WholeBody), + ] { + let (events, received) = exported(level, || { + let (outcome, delivered) = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(run_response_middleware_relay( + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello", + method, + script, + )) + }); + if method == "HEAD" + || matches!(script, ResponseRelayScript::InvalidWholeBodySequence) + { + assert!(matches!(outcome.unwrap(), RelayOutcome::Consumed)); + assert!(delivered.starts_with(b"HTTP/1.1 502 Bad Gateway\r\n")); + } else { + assert!(outcome.is_err()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!(!String::from_utf8_lossy(&delivered).contains("502 Bad Gateway")); + } + }) + .await; + let terminal = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("terminal delivery failure is recorded"); + let companions: Vec<_> = events + .iter() + .filter(|event| { + event.get("finding_info").is_some() + || event["unmapped"]["failed"] == true + || event["action_id"] == openshell_ocsf::ActionId::Denied as u8 + }) + .cloned() + .collect(); + assert!(companions.len() >= 2, "failure includes diagnostic records"); + assert_eq!( + deny_spans(&received).len(), + 1, + "one response stop is one exported decision" + ); + assert!(terminal.get("trace").is_none()); + assert_deny_resolves(&companions, &received, true); + } + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn credential_refusal_findings_record_denial_without_http_spans() { + let request = L7Request { + action: "POST".into(), + target: "/api".into(), + query_params: HashMap::new(), + raw_header: vec![], + body_length: BodyLength::None, + }; + let (events, received) = crate::telemetry::tests::exported("warn", || { + crate::l7::emit_uninspected_credential_finding("example.test", "p", "test"); + emit_uninspected_body_credential_denial( + &request, + &RelayRequestOptions { + host: "example.test", + port: 443, + ..Default::default() + }, + BodyCredentialError::KnownUnavailable, + ); + }) + .await; + assert_eq!(events.len(), 3); + assert_eq!( + events + .iter() + .filter(|event| event["class_uid"] == 2004) + .count(), + 2 + ); + for event in &events { + assert_eq!(event["action_id"], 2); + assert_eq!(event["action"], "Denied"); + } + crate::telemetry::tests::assert_no_deny(&events, &received); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_upstream_io_failure_is_not_an_enforced_denial() { + struct FailedReader; + impl AsyncRead for FailedReader { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + _buf: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::ConnectionReset, + "upstream response interrupted", + ))) + } + } + + for script in [ResponseRelayScript::Stream, ResponseRelayScript::WholeBody] { + let (events, received) = crate::telemetry::tests::exported("warn", || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let (runner, chain) = response_middleware_fixture(script); + let mut upstream = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\n" + .as_slice() + .chain(FailedReader); + let mut delivered = Vec::new(); + let outcome = relay_response( + "GET", + &mut upstream, + &mut delivered, + RelayResponseOptions::default(), + Some(response_middleware_context(&runner, &chain, "GET")), + ) + .await; + if matches!(script, ResponseRelayScript::Stream) { + assert!(outcome.is_err()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!( + !String::from_utf8_lossy(&delivered).contains("502 Bad Gateway") + ); + } else { + assert!(matches!(outcome.unwrap(), RelayOutcome::Consumed)); + assert!(delivered.starts_with(b"HTTP/1.1 502 Bad Gateway\r\n")); + } + }); + }); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("upstream I/O failure retains its diagnostic finding"); + assert!(finding.get("action_id").is_none()); + assert!(finding.get("action").is_none()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_downstream_body_failure_is_not_an_enforced_denial() { + #[derive(Default)] + struct DisconnectAfterHead(Vec); + impl AsyncWrite for DisconnectAfterHead { + fn poll_write( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if self.0.ends_with(b"\r\n\r\n") { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "client disconnected", + ))); + } + self.0.extend_from_slice(buf); + Poll::Ready(Ok(buf.len())) + } + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn poll_shutdown( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + } + + let (events, received) = crate::telemetry::tests::exported("warn", || { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let (runner, chain) = response_middleware_fixture(ResponseRelayScript::Stream); + let mut upstream = + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello".as_slice(); + let mut delivered = DisconnectAfterHead::default(); + let outcome = relay_response( + "GET", + &mut upstream, + &mut delivered, + RelayResponseOptions::default(), + Some(response_middleware_context(&runner, &chain, "GET")), + ) + .await; + assert!(outcome.is_err()); + assert!(delivered.0.starts_with(b"HTTP/1.1 200 OK\r\n")); + assert!(!String::from_utf8_lossy(&delivered.0).contains("502 Bad Gateway")); + }); + }); + }) + .await; + let finding = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_failure" + }) + .expect("downstream I/O failure retains its diagnostic finding"); + assert!(finding.get("action_id").is_none()); + assert!(finding.get("action").is_none()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_trace_allowed_and_fail_open_controls_do_not_create_deny_spans() { + for script in [ + ResponseRelayScript::Stream, + ResponseRelayScript::InvalidBodySequence, + ResponseRelayScript::InvalidWholeBodySequence, + ] { + let (events, received) = crate::telemetry::tests::exported("warn", || { + let (outcome, delivered) = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on( + run_response_middleware_relay_with_error( + b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello", + "GET", + script, + openshell_supervisor_middleware::OnError::FailOpen, + ), + ) + }); + assert!(outcome.is_ok()); + assert!(delivered.starts_with(b"HTTP/1.1 200 OK\r\n")); + }) + .await; + assert!(!events.is_empty()); + crate::telemetry::tests::assert_no_deny(&events, &received); + } + } + #[tokio::test] async fn response_middleware_headers_only_mutates_head_and_preserves_framing() { let (outcome, delivered) = run_response_middleware_relay( diff --git a/crates/openshell-supervisor-network/src/l7/rest/http_response.rs b/crates/openshell-supervisor-network/src/l7/rest/http_response.rs index 3efc323bdb..0113dc4323 100644 --- a/crates/openshell-supervisor-network/src/l7/rest/http_response.rs +++ b/crates/openshell-supervisor-network/src/l7/rest/http_response.rs @@ -408,16 +408,27 @@ where allowed = preflight.allowed, "HTTP response middleware preflight completed" ); - for event in crate::l7::middleware::middleware_finding_events(&preflight.findings) { - openshell_ocsf::ocsf_emit!(event); - } + let mut preflight_events = + crate::l7::middleware::middleware_finding_events(&preflight.findings); if !preflight.allowed { - emit_http_response_middleware_invocations( + preflight_events.extend(http_response_decision_events( middleware.policy_name, &middleware.target, status_code, &preflight.invocations, - ); + )); + if preflight.denial.is_none() { + preflight_events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + false, + true, + )); + } + } + crate::telemetry::emit_decision(preflight_events, None); + if !preflight.allowed { if let Some(denial) = preflight.denial.as_ref() { send_response_middleware_denial( client, @@ -428,12 +439,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -444,11 +449,14 @@ where } return Ok(Some(RelayOutcome::Consumed)); } - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &preflight.invocations, + crate::telemetry::emit_decision( + http_response_decision_events( + middleware.policy_name, + &middleware.target, + status_code, + &preflight.invocations, + ), + None, ); let Some(mut session) = preflight.session else { @@ -482,12 +490,21 @@ where Ok(finish) => finish, Err(error) => { debug!(error = %error, "HTTP response middleware finalization failed"); - emit_http_response_diagnostics( + let mut events = http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - &error.diagnostics, + &error.diagnostics.invocations, + &error.diagnostics.findings, ); + events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + false, + true, + )); + crate::telemetry::emit_decision(events, None); send_response_delivery_failure( client, request_method, @@ -502,15 +519,16 @@ where guard.ensure_current()?; } let mut headers = preflight.headers; - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &finish.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + middleware.policy_name, + &middleware.target, + status_code, + &finish.invocations, + &finish.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&finish.findings) { - openshell_ocsf::ocsf_emit!(event); - } if finish.strip_stale_integrity_headers { strip_response_integrity_headers(&mut headers); } @@ -610,13 +628,30 @@ where openshell_core::proto::MiddlewareSessionEndReason::UpstreamDisconnect }; session.end(end_reason).await; - if committed { - emit_http_response_middleware_failure( + // Diagnostics from a middleware stop and its terminal finding are + // one decision. Transport/framing errors still record delivery + // failure, but do not imply an enforced middleware refusal. + let mut events = middleware_stop.map_or_else(Vec::new, |stop| { + http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - true, - ); + &stop.diagnostics.invocations, + &stop.diagnostics.findings, + ) + }); + // A denial sent before commitment is reported by its invocation. + if committed || middleware_stop.is_none_or(|stop| stop.failure.denial.is_none()) { + events.push(http_response_middleware_failure_event( + middleware.policy_name, + &middleware.target, + status_code, + committed, + middleware_stop.is_some(), + )); + } + crate::telemetry::emit_decision(events, None); + if committed { return Err(error); } debug!(error = %error, "HTTP response processing failed before commitment"); @@ -630,12 +665,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -660,19 +689,24 @@ where let finish = match session.finish(trailers).await { Ok(finish) => finish, Err(error) => { - emit_http_response_diagnostics( + let mut events = http_response_diagnostic_events( middleware.policy_name, &middleware.target, status_code, - &error.diagnostics, + &error.diagnostics.invocations, + &error.diagnostics.findings, ); - if committed { - emit_http_response_middleware_failure( + if committed || error.denial.is_none() { + events.push(http_response_middleware_failure_event( middleware.policy_name, &middleware.target, status_code, + committed, true, - ); + )); + } + crate::telemetry::emit_decision(events, None); + if committed { return Err(miette!( "HTTP response middleware failed after commitment: {error}" )); @@ -688,12 +722,6 @@ where ) .await?; } else { - emit_http_response_middleware_failure( - middleware.policy_name, - &middleware.target, - status_code, - false, - ); send_response_delivery_failure( client, request_method, @@ -708,15 +736,16 @@ where if let Some(guard) = middleware.generation_guard { guard.ensure_current()?; } - emit_http_response_middleware_invocations( - middleware.policy_name, - &middleware.target, - status_code, - &finish.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + middleware.policy_name, + &middleware.target, + status_code, + &finish.invocations, + &finish.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&finish.findings) { - openshell_ocsf::ocsf_emit!(event); - } if whole_body && !committed { let mut headers = preflight.headers; @@ -861,29 +890,39 @@ where }) } -fn emit_http_response_middleware_invocations( +fn http_response_decision_events( policy_name: &str, target: &HttpRequestTarget, status_code: u16, invocations: &[openshell_supervisor_middleware::HttpResponseInvocation], -) { - for event in - http_response_middleware_invocation_events(policy_name, target, status_code, invocations) - { - openshell_ocsf::ocsf_emit!(event); - } +) -> Vec { + let mut events = + http_response_middleware_invocation_events(policy_name, target, status_code, invocations); for invocation in invocations { - if let Some(event) = - http_response_middleware_fail_open_finding_event(policy_name, target, invocation) - { - openshell_ocsf::ocsf_emit!(event); - } - if let Some(event) = - http_response_middleware_block_finding_event(policy_name, target, invocation) - { - openshell_ocsf::ocsf_emit!(event); - } - } + events.extend(http_response_middleware_fail_open_finding_event( + policy_name, + target, + invocation, + )); + events.extend(http_response_middleware_block_finding_event( + policy_name, + target, + invocation, + )); + } + events +} + +fn http_response_diagnostic_events( + policy_name: &str, + target: &HttpRequestTarget, + status_code: u16, + invocations: &[openshell_supervisor_middleware::HttpResponseInvocation], + findings: &[openshell_supervisor_middleware::NamespacedFinding], +) -> Vec { + let mut events = http_response_decision_events(policy_name, target, status_code, invocations); + events.extend(crate::l7::middleware::middleware_finding_events(findings)); + events } fn emit_http_response_diagnostics( @@ -892,15 +931,16 @@ fn emit_http_response_diagnostics( status_code: u16, diagnostics: &openshell_supervisor_middleware::HttpResponseDiagnostics, ) { - emit_http_response_middleware_invocations( - policy_name, - target, - status_code, - &diagnostics.invocations, + crate::telemetry::emit_decision( + http_response_diagnostic_events( + policy_name, + target, + status_code, + &diagnostics.invocations, + &diagnostics.findings, + ), + None, ); - for event in crate::l7::middleware::middleware_finding_events(&diagnostics.findings) { - openshell_ocsf::ocsf_emit!(event); - } } pub(super) fn http_response_middleware_invocation_events( @@ -914,8 +954,11 @@ pub(super) fn http_response_middleware_invocation_events( .map(|invocation| { let outcome = format!("{:?}", invocation.outcome).to_ascii_lowercase(); let failed = invocation.failed; - let blocked = invocation.outcome - == openshell_supervisor_middleware::HttpResponseInvocationOutcome::BlockDelivery; + let blocked = matches!( + invocation.outcome, + openshell_supervisor_middleware::HttpResponseInvocationOutcome::BlockDelivery + | openshell_supervisor_middleware::HttpResponseInvocationOutcome::FailClosed + ); openshell_ocsf::HttpActivityBuilder::new(ocsf_ctx()) .activity(openshell_ocsf::ActivityId::Other) .action(if blocked { @@ -991,6 +1034,7 @@ fn http_response_middleware_block_finding_event( } Some( openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()) + .action(openshell_ocsf::ActionId::Denied) .severity(openshell_ocsf::SeverityId::Medium) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.middleware.http_response_blocked", @@ -1068,8 +1112,28 @@ fn emit_http_response_middleware_failure( status_code: u16, committed: bool, ) { + crate::telemetry::egress_emit!(http_response_middleware_failure_event( + policy_name, + target, + status_code, + committed, + true, + )); +} + +fn http_response_middleware_failure_event( + policy_name: &str, + target: &HttpRequestTarget, + status_code: u16, + committed: bool, + denied: bool, +) -> openshell_ocsf::OcsfEvent { let status_code = status_code.to_string(); - let event = openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()) + let mut builder = openshell_ocsf::DetectionFindingBuilder::new(ocsf_ctx()); + if denied { + builder = builder.action(openshell_ocsf::ActionId::Denied); + } + builder .severity(openshell_ocsf::SeverityId::High) .finding_info(openshell_ocsf::FindingInfo::new( "openshell.middleware.http_response_failure", @@ -1093,8 +1157,7 @@ fn emit_http_response_middleware_failure( } else { "HTTP response middleware failed before response commitment" }) - .build(); - openshell_ocsf::ocsf_emit!(event); + .build() } #[derive(Debug)] @@ -1627,11 +1690,18 @@ struct ResponseOutputState<'a> { #[derive(Debug)] struct ResponseMiddlewareStop { failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure, + diagnostics: openshell_supervisor_middleware::HttpResponseDiagnostics, } impl ResponseMiddlewareStop { - fn new(failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure) -> Self { - Self { failure } + fn new( + failure: openshell_supervisor_middleware::HttpResponseMiddlewareFailure, + diagnostics: openshell_supervisor_middleware::HttpResponseDiagnostics, + ) -> Self { + Self { + failure, + diagnostics, + } } } @@ -1676,13 +1746,15 @@ async fn expire_whole_body_deadline( guard.ensure_current()?; } let diagnostics = session.take_diagnostics(); - emit_http_response_diagnostics( - framing.policy_name, - framing.target, - framing.status_code, - &diagnostics, - ); - let output = output.map_err(ResponseMiddlewareStop::new)?; + if output.is_ok() { + emit_http_response_diagnostics( + framing.policy_name, + framing.target, + framing.status_code, + &diagnostics, + ); + } + let output = output.map_err(|failure| ResponseMiddlewareStop::new(failure, diagnostics))?; deliver_response_units(client, output, framing, session.requires_whole_body()).await } @@ -1818,13 +1890,15 @@ async fn process_response_unit( guard.ensure_current()?; } let diagnostics = session.take_diagnostics(); - emit_http_response_diagnostics( - framing.policy_name, - framing.target, - framing.status_code, - &diagnostics, - ); - let output = output.map_err(ResponseMiddlewareStop::new)?; + if output.is_ok() { + emit_http_response_diagnostics( + framing.policy_name, + framing.target, + framing.status_code, + &diagnostics, + ); + } + let output = output.map_err(|failure| ResponseMiddlewareStop::new(failure, diagnostics))?; deliver_response_units(client, output, framing, session.requires_whole_body()).await } @@ -2081,6 +2155,137 @@ pub(super) fn response_is_event_stream(headers: &str) -> bool { #[cfg(test)] mod tests { + use openshell_supervisor_middleware::{ + HttpResponseInvocation, HttpResponseInvocationOutcome as Outcome, NamespacedFinding, + }; + + fn invocation(outcome: Outcome, failed: bool) -> HttpResponseInvocation { + HttpResponseInvocation { + config_name: "guard".to_string(), + implementation: "test/guard".to_string(), + outcome, + sequence: None, + input_size: 0, + output_size: None, + failed, + stage_disabled: false, + reason_code: None, + failure_category: None, + } + } + + fn finding() -> NamespacedFinding { + NamespacedFinding { + middleware: "guard".to_string(), + finding: openshell_core::proto::Finding { + r#type: "test.finding".to_string(), + label: "Test finding".to_string(), + count: 1, + severity: "high".to_string(), + ..Default::default() + }, + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_terminal_failure_without_diagnostics_has_no_deny_trace() { + use crate::telemetry::tests::{assert_no_deny, exported}; + + // Platform refusals can lack invocation diagnostics, but must retain + // their terminal failure record. Without an HTTP record, no deny span. + for committed in [false, true] { + let (events, received) = exported("warn", || { + super::emit_http_response_middleware_failure( + "response-policy", + &super::HttpRequestTarget::default(), + 200, + committed, + ); + }) + .await; + + assert_eq!(events.len(), 1, "only the terminal failure is emitted"); + assert_eq!( + events[0]["finding_info"]["uid"], + "openshell.middleware.http_response_failure" + ); + assert_no_deny(&events, &received); + } + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn response_diagnostic_findings_share_a_deny_span_only_when_delivery_is_denied() { + use crate::telemetry::tests::{assert_deny_resolves, assert_no_deny, exported}; + + for denied in [false, true] { + let (events, received) = exported("warn", || { + let outcome = if denied { + Outcome::BlockDelivery + } else { + Outcome::PassThrough + }; + let diagnostics = openshell_supervisor_middleware::HttpResponseDiagnostics { + invocations: vec![invocation(outcome, false)], + findings: vec![finding()], + ..Default::default() + }; + super::emit_http_response_diagnostics( + "response-policy", + &super::HttpRequestTarget::default(), + 200, + &diagnostics, + ); + }) + .await; + // A blocking invocation adds its own block finding. + assert_eq!(events.len(), if denied { 3 } else { 2 }); + if denied { + let blocked = events + .iter() + .find(|event| { + event["finding_info"]["uid"] == "openshell.middleware.http_response_blocked" + }) + .expect("blocked delivery emits its refusal finding"); + assert_eq!(blocked["action_id"], 2); + assert_eq!(blocked["action"], "Denied"); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); + } else { + assert_no_deny(&events, &received); + } + } + } + + #[test] + fn fail_closed_invocations_and_enforced_failures_record_the_denial() { + let target = super::HttpRequestTarget::default(); + for (outcome, failed, action_id) in [ + (Outcome::BlockDelivery, false, 2), + (Outcome::FailClosed, true, 2), + (Outcome::FailOpen, true, 99), + (Outcome::PassThrough, false, 1), + ] { + let events = super::http_response_middleware_invocation_events( + "p", + &target, + 200, + &[invocation(outcome, failed)], + ); + assert_eq!( + events[0].to_json().unwrap()["action_id"], + action_id, + "{outcome:?}" + ); + } + for denied in [false, true] { + let failure = + super::http_response_middleware_failure_event("p", &target, 200, false, denied) + .to_json() + .unwrap(); + assert_eq!(failure["action_id"] == 2, denied); + } + } + use super::{ ResponseFraming, parse_connection_keep_alive, parse_response_head_for_middleware, response_is_http_10, serialize_response_head, diff --git a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs index 65d7b70d08..f868575097 100644 --- a/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs +++ b/crates/openshell-supervisor-network/src/l7/token_grant_injection.rs @@ -22,12 +22,13 @@ use openshell_core::token_grant_failure::{ }; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId, - StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + StatusId, ctx::ctx as ocsf_ctx, }; use tracing::warn; use crate::l7::provider::L7Request; use crate::l7::relay::L7EvalContext; +use crate::telemetry::egress_emit; /// Borrowed grant configuration for one endpoint-bound provider credential. pub struct TokenGrantRequest<'a> { @@ -340,7 +341,7 @@ fn report_refused_grant( .map_or_else(|| "none".to_string(), |status| status.to_string()), "Token grant failed" ); - ocsf_emit!( + egress_emit!( HttpActivityBuilder::new(ocsf_ctx()) .activity(ActivityId::Fail) .action(ActionId::Denied) @@ -398,7 +399,7 @@ async fn inject_selected( inject_header(&raw_header, &name, &value).map_err(TokenGrantInjectionError::new)?; } for (provider_key, _) in credentials { - ocsf_emit!( + egress_emit!( HttpActivityBuilder::new(ocsf_ctx()) .activity(ActivityId::Other) .action(ActionId::Allowed) diff --git a/crates/openshell-supervisor-network/src/l7/websocket.rs b/crates/openshell-supervisor-network/src/l7/websocket.rs index 5370e383c3..4d688929c6 100644 --- a/crates/openshell-supervisor-network/src/l7/websocket.rs +++ b/crates/openshell-supervisor-network/src/l7/websocket.rs @@ -9,13 +9,13 @@ use crate::l7::relay::{L7EvalContext, evaluate_l7_request}; use crate::l7::{EnforcementMode, L7RequestInfo}; use crate::opa::{PolicyGenerationGuard, TunnelPolicyEngine}; +use crate::telemetry::egress_emit; use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress, Status}; use miette::{IntoDiagnostic, Result, miette}; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_core::secrets::{SecretResolver, contains_reserved_credential_marker}; use openshell_ocsf::{ ActionId, ActivityId, DispositionId, Endpoint, NetworkActivityBuilder, SeverityId, StatusId, - ocsf_emit, }; use std::collections::HashMap; use std::future::Future; @@ -584,7 +584,7 @@ where if let Err(termination) = &result { let error = termination.error.to_string(); if error.contains(CREDENTIAL_PLACEMENT_MISMATCH) { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Fail) .action(ActionId::Denied) @@ -597,7 +597,7 @@ where .status_detail("credential_placement_mismatch") .build() ); - ocsf_emit!(crate::l7::build_credential_placement_mismatch_finding( + egress_emit!(crate::l7::build_credential_placement_mismatch_finding( options.policy_name, host, Some("websocket"), @@ -641,7 +641,7 @@ async fn write_policy_violation_close(writer: &mut W) -> } fn emit_credential_endpoint_mismatch(host: &str, port: u16, policy_name: &str) { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Fail) .action(ActionId::Denied) @@ -656,7 +656,7 @@ fn emit_credential_endpoint_mismatch(host: &str, port: u16, policy_name: &str) { .status_detail("credential_endpoint_mismatch") .build() ); - ocsf_emit!(crate::l7::build_credential_endpoint_mismatch_finding( + egress_emit!(crate::l7::build_credential_endpoint_mismatch_finding( policy_name, host, Some("websocket"), @@ -1412,7 +1412,7 @@ fn emit_uninspected_credential_denial(host: &str, port: u16, policy_name: &str, "WebSocket credential traffic denied for {host}:{port}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); crate::l7::emit_uninspected_credential_finding(host, policy_name, surface); } @@ -2087,7 +2087,7 @@ fn emit_rewrite_event(host: &str, port: u16, policy_name: &str, replacements: us .firewall_rule(policy_name, "l7-websocket") .message(rewrite_event_message(host, port, replacements)) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn rewrite_event_message(host: &str, port: u16, replacements: usize) -> String { @@ -2140,7 +2140,7 @@ fn emit_websocket_l7_event( request_info.action, request_info.target, summary )) .build(); - ocsf_emit!(event); + egress_emit!(event); } fn observe_termination( @@ -2153,7 +2153,7 @@ fn observe_termination( emit_protocol_failure(host, port, policy_name, failure_class); } if termination.cause == WebSocketTerminationCause::CapacityExhausted { - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2177,7 +2177,7 @@ fn emit_protocol_failure( policy_name: &str, failure_class: FrameFailureClass, ) { - ocsf_emit!(protocol_failure_event( + egress_emit!(protocol_failure_event( host, port, policy_name, diff --git a/crates/openshell-supervisor-network/src/lib.rs b/crates/openshell-supervisor-network/src/lib.rs index d9fcd004b6..74a6698b92 100644 --- a/crates/openshell-supervisor-network/src/lib.rs +++ b/crates/openshell-supervisor-network/src/lib.rs @@ -24,6 +24,7 @@ pub mod proxy; pub mod run; pub mod sigv4; mod spiffe_endpoint; +mod telemetry; mod token_grant; pub mod upstream_proxy; diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 2d1a73a949..e6ec6ead5f 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -145,7 +145,7 @@ fn emit_binary_identity_mode(require_binary_identity: bool, source: &str) { require_binary_identity, source, "Configured OPA runtime binary identity mode" ); - openshell_ocsf::ocsf_emit!( + crate::telemetry::egress_emit!( openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(openshell_ocsf::SeverityId::Informational) .status(openshell_ocsf::StatusId::Success) @@ -1427,7 +1427,7 @@ fn parse_process_policy(val: ®orus::Value) -> ProcessPolicy { fn emit_l7_config_warnings(warnings: &[String], prefix: &str) { for w in warnings { - openshell_ocsf::ocsf_emit!( + crate::telemetry::egress_emit!( openshell_ocsf::ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(openshell_ocsf::SeverityId::Medium) .status(openshell_ocsf::StatusId::Success) diff --git a/crates/openshell-supervisor-network/src/policy_dns/mod.rs b/crates/openshell-supervisor-network/src/policy_dns/mod.rs index 5ddbd25cf4..4d5b61895d 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/mod.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/mod.rs @@ -35,10 +35,11 @@ pub(crate) use store::{ use crate::opa::OpaEngine; use crate::proxy::destination::{build_validation_plan, filter_resolved_addresses}; use crate::proxy::is_host_gateway_alias; +use crate::telemetry::egress_emit; use openshell_core::host_pattern::HostSelector; use openshell_ocsf::{ ActionId, ActivityId, ConfigStateChangeBuilder, DispositionId, Endpoint, - NetworkActivityBuilder, SeverityId, StateId, StatusId, ocsf_emit, + NetworkActivityBuilder, SeverityId, StateId, StatusId, }; use sha2::{Digest, Sha256}; use std::sync::Arc; @@ -493,7 +494,7 @@ fn dns_query_endpoint(name: &NormalizedName) -> Endpoint { } fn emit_dns_denial(name: &NormalizedName, detail: &str, message: &str) { - ocsf_emit!(build_dns_denial_event(name, detail, message)); + egress_emit!(build_dns_denial_event(name, detail, message)); } fn build_dns_denial_event( @@ -580,7 +581,7 @@ fn emit_dns_failure( detail: &str, message: &str, ) { - ocsf_emit!(build_dns_failure_event( + egress_emit!(build_dns_failure_event( name, family, eligible_endpoints, @@ -591,11 +592,11 @@ fn emit_dns_failure( } fn emit_mapping_publication(record: &ResolvedEndpointRecord) { - ocsf_emit!(build_mapping_publication_event(record)); + egress_emit!(build_mapping_publication_event(record)); } fn emit_observation_publication(record: &ResolvedEndpointRecord) { - ocsf_emit!(build_observation_publication_event(record)); + egress_emit!(build_observation_publication_event(record)); } fn build_observation_publication_event( diff --git a/crates/openshell-supervisor-network/src/policy_dns/runtime.rs b/crates/openshell-supervisor-network/src/policy_dns/runtime.rs index cf1f61cfca..d7888f991e 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/runtime.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/runtime.rs @@ -14,7 +14,7 @@ use futures::{FutureExt as _, StreamExt as _, stream::FuturesUnordered}; use miette::{IntoDiagnostic, Result, WrapErr}; use openshell_core::net::set_tcp_nodelay_best_effort; use openshell_isolation_interface::contract::{DnsTransport, PendingDnsQuery}; -use openshell_ocsf::{ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ocsf_emit}; +use openshell_ocsf::{ConfigStateChangeBuilder, SeverityId, StateId, StatusId}; use openshell_sandbox_backend::NetworkMediationSource; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::sync::Arc; @@ -169,7 +169,7 @@ impl PolicyDnsRuntime { let _ = expiry_store.expire(std::time::Instant::now()); } }); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -284,7 +284,7 @@ impl PolicyDnsRuntime { } }); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) diff --git a/crates/openshell-supervisor-network/src/policy_dns/store.rs b/crates/openshell-supervisor-network/src/policy_dns/store.rs index 9c76e89c5e..62fe1c19bf 100644 --- a/crates/openshell-supervisor-network/src/policy_dns/store.rs +++ b/crates/openshell-supervisor-network/src/policy_dns/store.rs @@ -433,7 +433,7 @@ impl ResolvedEndpointStore { if allocated.saturating_mul(5) >= capacity.saturating_mul(4) && !emitted.swap(true, Ordering::Relaxed) { - openshell_ocsf::ocsf_emit!(build_pool_high_water_event( + crate::telemetry::egress_emit!(build_pool_high_water_event( allocated, capacity, family, )); } diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 033f492aa7..bdf8898986 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -12,7 +12,7 @@ use openshell_core::proto::{ SandboxPolicy as ProtoSandboxPolicy, }; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ocsf_emit, sanitize_reason_for_audit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, sanitize_reason_for_audit, }; use serde::Deserialize; use std::collections::HashMap; @@ -721,7 +721,7 @@ async fn submit_proposal(ctx: &PolicyLocalContext, body: &[u8]) -> (u16, serde_j /// and the endpoint the agent is asking to reach — what a developer needs /// to see in the audit trace to correlate against the inbox card. fn emit_policy_propose_event(chunk_id: &str, summary: &str) { - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -741,7 +741,7 @@ fn emit_policy_propose_event(chunk_id: &str, summary: &str) { fn emit_policy_decision_event(chunk: &PolicyChunk) { let summary = summarize_chunk_for_audit(chunk); match chunk.status.as_str() { - "approved" => ocsf_emit!( + "approved" => crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -763,7 +763,7 @@ fn emit_policy_decision_event(chunk: &PolicyChunk) { } else { format!("\"{sanitized}\"") }; - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(openshell_ocsf::ctx::ctx()) .severity(SeverityId::Low) .status(StatusId::Success) diff --git a/crates/openshell-supervisor-network/src/proxy.rs b/crates/openshell-supervisor-network/src/proxy.rs index 85196d9f44..96c0b8f50a 100644 --- a/crates/openshell-supervisor-network/src/proxy.rs +++ b/crates/openshell-supervisor-network/src/proxy.rs @@ -8,6 +8,8 @@ mod egress; mod relay; #[cfg(test)] pub(crate) use relay::relay_inspected_http_stream_for_test; +#[cfg(test)] +mod trace_tests; #[cfg(test)] use openshell_sandbox_backend::test_support as rpc_test; @@ -19,6 +21,7 @@ use crate::policy_dns::{ MappingLookup, MappingLookupError, PolicyEndpointId, ResolvedEndpointStore, }; use crate::policy_local::{POLICY_LOCAL_HOST, PolicyLocalContext}; +use crate::telemetry::egress_emit; use crate::upstream_proxy::{self, UpstreamProxyConfig}; use futures::{FutureExt as _, StreamExt as _, stream::FuturesUnordered}; use http::StatusCode; @@ -44,7 +47,7 @@ use openshell_isolation_interface::contract::{ use openshell_ocsf::{ ActionId, ActivityId, BaseEventBuilder, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, HttpResponse, NetworkActivityBuilder, Process, SeverityId, StatusId, - Url as OcsfUrl, ocsf_emit, + Url as OcsfUrl, }; use openshell_sandbox_backend::NetworkMediationSource; use std::future::Future; @@ -244,16 +247,21 @@ fn build_credential_endpoint_mismatch_event( .build() } -fn emit_credential_endpoint_mismatch(method: &str, host: &str, port: u16, policy_name: &str) { +fn emit_credential_endpoint_mismatch( + method: &str, + host: &str, + port: u16, + policy_name: &str, + connect_span: &tracing::Span, +) { let event = build_credential_endpoint_mismatch_event(method, host, port, policy_name); - ocsf_emit!(event); let finding = crate::l7::build_credential_endpoint_mismatch_finding( policy_name, host, None, "Provider credential endpoint binding mismatch; request denied", ); - ocsf_emit!(finding); + crate::telemetry::emit_decision([event, finding], Some(connect_span)); } /// Hostnames injected by compute drivers as `/etc/hosts` aliases for the host @@ -349,7 +357,7 @@ impl ProxyHandle { format!("Proxy listening on {local_addr}") }) .build(); - ocsf_emit!(event); + egress_emit!(event); } // Detect the trusted host gateway IP from /etc/hosts before user code @@ -390,7 +398,7 @@ impl ProxyHandle { refusing to start: {err}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); miette::miette!("invalid upstream corporate proxy configuration: {err}") })?, ); @@ -404,7 +412,7 @@ impl ProxyHandle { cfg.summary() )) .build(); - ocsf_emit!(event); + egress_emit!(event); } let (exited_tx, exited_rx) = tokio::sync::oneshot::channel::<()>(); @@ -574,7 +582,7 @@ impl ProxyHandle { ) .await { - ocsf_emit!(build_proxy_connection_error_event( + egress_emit!(build_proxy_connection_error_event( peer_addr, transparent_destination_addr, format!("Proxy connection error: {err}"), @@ -583,7 +591,7 @@ impl ProxyHandle { }); } Err(ProxyAcceptError::Source(err)) => { - ocsf_emit!(build_mediation_lane_failure_event(format!( + egress_emit!(build_mediation_lane_failure_event(format!( "Network-mediation source failed; proxy accept loop exiting: {err}" ))); break; @@ -594,7 +602,7 @@ impl ProxyHandle { &mut consecutive_resource_errors, &mut consecutive_unknown_errors, ); - ocsf_emit!(build_accept_error_event(local_addr, &err, &action)); + egress_emit!(build_accept_error_event(local_addr, &err, &action)); match action { AcceptAction::Terminal => break, AcceptAction::Retry { backoff, .. } => { @@ -946,7 +954,7 @@ fn emit_staged_transparent_denial( reason: &str, status_detail: &'static str, ) { - ocsf_emit!(build_staged_transparent_denial_event( + egress_emit!(build_staged_transparent_denial_event( destination, mapped_host, identity, @@ -1180,7 +1188,7 @@ impl TransparentTcpHandle { ) .await { - ocsf_emit!(build_connection_error_event( + egress_emit!(build_connection_error_event( peer_addr, format!("Transparent TCP connection error: {error}") )); @@ -1416,7 +1424,7 @@ async fn handle_transparent_tcp_connection( let pid = decision .binary_pid .map_or_else(|| "-".to_string(), |pid| pid.to_string()); - ocsf_emit!(build_transparent_tcp_allow_ocsf_event( + egress_emit!(build_transparent_tcp_allow_ocsf_event( TransparentTcpAllowAudit { workload: workload_addr, synthetic_destination: original, @@ -1560,7 +1568,7 @@ fn emit_transparent_mapping_denial( | MappingLookupError::InvalidMapping | MappingLookupError::LockPoisoned => "transparent_tcp_destination_denied", }; - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -1594,7 +1602,7 @@ fn emit_transparent_policy_denial( let pid = decision .binary_pid .map_or_else(|| "-".to_string(), |pid| pid.to_string()); - ocsf_emit!( + egress_emit!( NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(ActivityId::Open) .action(ActionId::Denied) @@ -2066,6 +2074,61 @@ fn build_forward_allow_ocsf_event( .build() } +fn build_forward_rejection_event( + host: &str, + port: u16, + message: String, +) -> openshell_ocsf::OcsfEvent { + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) + .severity(SeverityId::Medium) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .message(message) + .build() +} + +fn build_forward_l7_evaluation_error( + host: &str, + port: u16, + enforcement: crate::l7::EnforcementMode, + error: &str, +) -> openshell_ocsf::OcsfEvent { + let enforced = enforcement == crate::l7::EnforcementMode::Enforce; + NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Fail) + .action(if enforced { + ActionId::Denied + } else { + ActionId::Other + }) + .disposition(if enforced { + DispositionId::Blocked + } else { + DispositionId::Allowed + }) + .severity(SeverityId::Low) + .status(StatusId::Failure) + .dst_endpoint(Endpoint::from_domain(host, port)) + .message(format!("L7 evaluation failed: {error}")) + .build() +} + +fn emit_forward_l7_decision( + decision: openshell_ocsf::OcsfEvent, + evaluation_error: Option, + connect_span: &tracing::Span, +) { + // The HTTP decision carries policy context that its evaluation-error + // companion lacks, so it must supply the shared deny span's attributes. + crate::telemetry::emit_decision( + std::iter::once(decision).chain(evaluation_error), + Some(connect_span), + ); +} + fn build_forward_parse_error_ocsf_event( peer_addr: Option, method: &str, @@ -2080,6 +2143,8 @@ fn build_forward_parse_error_ocsf_event( .http_response(HttpResponse { code: StatusCode::BAD_REQUEST.as_u16(), }) + .action(ActionId::Denied) + .disposition(DispositionId::Blocked) .severity(SeverityId::Low) .status(StatusId::Failure) .message(format!("FORWARD parse error for {path}")); @@ -2324,7 +2389,7 @@ where C: TokioAsyncWrite + Unpin, { let detail = destination_denial_detail(denial.kind); - ocsf_emit!(build_connect_destination_deny_ocsf_event( + egress_emit!(build_connect_destination_deny_ocsf_event( denial, peer_addr, host, port, binary, pid, ancestors, cmdline, )); @@ -2357,6 +2422,7 @@ where #[allow(clippy::too_many_arguments)] async fn deny_forward_destination( client: &mut C, + connect_span: &tracing::Span, denial: &DestinationDenial, peer_addr: SocketAddr, method: &str, @@ -2376,9 +2442,12 @@ where C: TokioAsyncWrite + Unpin, { let detail = destination_denial_detail(denial.kind); - ocsf_emit!(build_forward_destination_deny_ocsf_event( - denial, peer_addr, method, host, port, path, binary, pid, ancestors, cmdline, policy, - )); + crate::telemetry::emit_decision( + [build_forward_destination_deny_ocsf_event( + denial, peer_addr, method, host, port, path, binary, pid, ancestors, cmdline, policy, + )], + Some(connect_span), + ); emit_denial_simple( denial_tx, @@ -2835,7 +2904,7 @@ async fn handle_mediated_connection( .message(format!("CONNECT denied {host_lc}:{port}")) .status_detail(&deny_reason) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_denial( &denial_tx, &host_lc, @@ -3000,7 +3069,7 @@ async fn handle_mediated_connection( )) .status_detail(TLS_TERMINATION_UNAVAILABLE_DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "tls_termination_unavailable"); emit_denial( &denial_tx, @@ -3038,7 +3107,7 @@ async fn handle_mediated_connection( )) .status_detail(DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); crate::l7::emit_uninspected_credential_finding( &host_lc, policy_str, @@ -3120,7 +3189,7 @@ async fn handle_mediated_connection( // Log the allowed CONNECT — use CONNECT_L7 when L7 inspection follows, // so log consumers can distinguish L4-only decisions from tunnel lifecycle events. - ocsf_emit!(build_connect_allow_ocsf_event( + egress_emit!(build_connect_allow_ocsf_event( workload_addr, &host_lc, port, @@ -3231,7 +3300,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message("Upstream TLS establishment failed") .build(); - ocsf_emit!(event); + egress_emit!(event); return Err(error); } }; @@ -3260,7 +3329,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message(format!("TLS relay error: {e}")) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } else { @@ -3292,7 +3361,7 @@ async fn handle_mediated_connection( .message(format!("CONNECT refused for {host_lc}:{port}: {DETAIL}")) .status_detail(DETAIL) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "tls_termination_unavailable"); emit_denial( &denial_tx, @@ -3335,7 +3404,7 @@ async fn handle_mediated_connection( .dst_endpoint(Endpoint::from_domain(&host_lc, port)) .message(message) .build(); - ocsf_emit!(event); + egress_emit!(event); } } } else { @@ -3365,7 +3434,7 @@ async fn handle_mediated_connection( )) .status_detail(protocol_detail) .build(); - ocsf_emit!(event); + egress_emit!(event); emit_activity_simple(activity_tx.as_ref(), true, "l7_parse_rejection"); emit_denial( &denial_tx, @@ -3966,7 +4035,7 @@ fn emit_l7_tunnel_close_after_policy_change(host: &str, port: u16, error: miette "L7 tunnel closed before inspection because policy changed: {error}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); } async fn reject_stale_connect_policy( @@ -4808,7 +4877,7 @@ fn parse_allowed_ips(raw: &[String]) -> std::result::Result, S n.prefix_len() )) .build(); - ocsf_emit!(event); + egress_emit!(event); } nets.push(n); } @@ -5482,7 +5551,7 @@ async fn handle_forward_proxy( // canonicalized below before credential binding, policy-path evaluation, // upstream bytes, or telemetry consume it. let Ok((scheme, host, port, mut path)) = parse_proxy_uri(target_uri) else { - ocsf_emit!(build_forward_parse_error_ocsf_event( + egress_emit!(build_forward_parse_error_ocsf_event( workload_peer_addr, method, &telemetry_path @@ -5551,7 +5620,7 @@ async fn handle_forward_proxy( if scheme != "http" { let event = build_forward_unsupported_scheme_ocsf_event(method, &scheme, &host_lc, port); - ocsf_emit!(event); + egress_emit!(event); if scheme == "https" { respond( client, @@ -5643,18 +5712,21 @@ async fn handle_forward_proxy( let matched_policy = match &decision.action { NetworkAction::Allow { matched_policy } => matched_policy.clone(), NetworkAction::Deny { reason } => { - ocsf_emit!(build_forward_policy_deny_ocsf_event( - workload_addr, - method, - &host_lc, - port, - &telemetry_path, - &binary_str, - &pid_str, - &ancestors_str, - &cmdline_str, - reason, - )); + crate::telemetry::emit_decision( + [build_forward_policy_deny_ocsf_event( + workload_addr, + method, + &host_lc, + port, + &telemetry_path, + &binary_str, + &pid_str, + &ancestors_str, + &cmdline_str, + reason, + )], + Some(&connect_span), + ); emit_denial_simple( denial_tx, &host_lc, @@ -5754,16 +5826,12 @@ async fn handle_forward_proxy( let prepared_target = match prepare_forward_target(&path, canonicalize_options) { Ok(prepared) => prepared, Err(error) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!( - "FORWARD rejecting non-canonical request-target: {error}" - )) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD rejecting non-canonical request-target: {error}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "forward_parse_rejection"); respond( client, @@ -5916,19 +5984,22 @@ async fn handle_forward_proxy( if !l7_config.config.allow_encoded_slash && crate::l7::path::canonical_path_has_encoded_slash(&path) { - ocsf_emit!(build_forward_l7_parse_rejection_ocsf_event( - workload_addr, - method, - &host_lc, - port, - &telemetry_path, - &binary_str, - &pid_str, - &ancestors_str, - &cmdline_str, - policy_str, - FORWARD_ENCODED_SLASH_REJECTION_DETAIL, - )); + crate::telemetry::emit_decision( + [build_forward_l7_parse_rejection_ocsf_event( + workload_addr, + method, + &host_lc, + port, + &telemetry_path, + &binary_str, + &pid_str, + &ancestors_str, + &cmdline_str, + policy_str, + FORWARD_ENCODED_SLASH_REJECTION_DETAIL, + )], + Some(&connect_span), + ); emit_activity_simple(activity_tx, true, "forward_parse_rejection"); respond( client, @@ -5973,7 +6044,7 @@ async fn handle_forward_proxy( )) .status_detail(upgrade_detail) .build(); - ocsf_emit!(event); + crate::telemetry::emit_decision([event], Some(&connect_span)); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); emit_denial_simple( denial_tx, @@ -6031,14 +6102,12 @@ async fn handle_forward_proxy( { Ok(info) => info, Err(e) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("FORWARD_GRAPHQL_L7 request rejected: {e}")) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD_GRAPHQL_L7 request rejected: {e}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); respond( client, @@ -6085,14 +6154,12 @@ async fn handle_forward_proxy( { Ok(body) => body, Err(e) => { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Medium) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("FORWARD_JSONRPC_L7 request rejected: {e}")) - .build(); - ocsf_emit!(event); + let event = build_forward_rejection_event( + &host_lc, + port, + format!("FORWARD_JSONRPC_L7 request rejected: {e}"), + ); + egress_emit!(event); emit_activity_simple(activity_tx, true, "l7_parse_rejection"); respond( client, @@ -6143,18 +6210,17 @@ async fn handle_forward_proxy( let hard_deny_reason = crate::l7::relay::l7_request_hard_deny_reason(l7_config.config.protocol, &request_info); let force_deny = hard_deny_reason.is_some(); + let mut evaluation_error = None; let (allowed, reason) = hard_deny_reason.map_or_else( || { crate::l7::relay::evaluate_l7_request(&tunnel_engine, &l7_ctx, &request_info) .unwrap_or_else(|e| { - let event = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) - .activity(ActivityId::Fail) - .severity(SeverityId::Low) - .status(StatusId::Failure) - .dst_endpoint(Endpoint::from_domain(&host_lc, port)) - .message(format!("L7 eval failed, denying request: {e}")) - .build(); - ocsf_emit!(event); + evaluation_error = Some(build_forward_l7_evaluation_error( + &host_lc, + port, + l7_config.config.enforcement, + &e.to_string(), + )); (false, format!("L7 evaluation error: {e}")) }) }, @@ -6230,7 +6296,7 @@ async fn handle_forward_proxy( .firewall_rule(policy_str, engine_type) .message(log_message) .build(); - ocsf_emit!(event); + emit_forward_l7_decision(event, evaluation_error, &connect_span); } let effectively_denied = force_deny @@ -6281,6 +6347,7 @@ async fn handle_forward_proxy( Err(denial) => { deny_forward_destination( client, + &connect_span, &denial, workload_addr, method, @@ -6319,6 +6386,7 @@ async fn handle_forward_proxy( Err(denial) => { deny_forward_destination( client, + &connect_span, &denial, workload_addr, method, @@ -6428,7 +6496,12 @@ async fn handle_forward_proxy( exchange: &middleware_exchange, l7_reevaluation, }; - forward_request_bytes = match pipeline.apply(request, client).await? { + // Middleware denials join the connect trace, like other FORWARD denials. + forward_request_bytes = match pipeline + .apply(request, client) + .instrument(connect_span.clone()) + .await? + { crate::l7::middleware::MiddlewareApplyResult::Allowed(request) => request.raw_header, crate::l7::middleware::MiddlewareApplyResult::Denied { denial, .. } => { emit_activity_simple(activity_tx, true, "middleware"); @@ -6609,13 +6682,21 @@ async fn handle_forward_proxy( .await; } if e.is_placement_mismatch() { - crate::l7::relay::reject_credential_resolution(client, &l7_ctx, method, &e).await?; + crate::l7::relay::reject_credential_resolution(client, &l7_ctx, method, &e) + .instrument(connect_span.clone()) + .await?; client.shutdown().await.into_diagnostic()?; } else if e.is_endpoint_mismatch() { if let Some(observer) = endpoint_observer.as_ref() { observer.observe_credential_failure(true); } - emit_credential_endpoint_mismatch(method, &host_lc, port, policy_str); + emit_credential_endpoint_mismatch( + method, + &host_lc, + port, + policy_str, + &connect_span, + ); respond( client, &build_json_error_response( @@ -6742,7 +6823,7 @@ async fn handle_forward_proxy( "FORWARD upstream connect failed for {host_lc}:{port}: {e}" )) .build(); - ocsf_emit!(event); + egress_emit!(event); if let Some(session) = middleware_session.take() { session .end(openshell_core::proto::MiddlewareSessionEndReason::UpstreamFailure) @@ -6872,7 +6953,7 @@ async fn handle_forward_proxy( // The request has now survived middleware, token grant, credential // rewriting, generation checks, and the HTTP relay. Only now record the // final allowed outcome. - ocsf_emit!(build_forward_allow_ocsf_event( + egress_emit!(build_forward_allow_ocsf_event( workload_addr, method, &host_lc, diff --git a/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs b/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs index 010cb466c5..3623b1dbb7 100644 --- a/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs +++ b/crates/openshell-supervisor-network/src/proxy/tests/compatibility.rs @@ -116,6 +116,7 @@ async fn destination_denials_preserve_adapter_specific_wire_contracts() { let (mut app, mut proxy) = tcp_pair().await; deny_forward_destination( &mut proxy, + &tracing::Span::none(), &denial, peer, "POST", diff --git a/crates/openshell-supervisor-network/src/proxy/trace_tests.rs b/crates/openshell-supervisor-network/src/proxy/trace_tests.rs new file mode 100644 index 0000000000..1602913885 --- /dev/null +++ b/crates/openshell-supervisor-network/src/proxy/trace_tests.rs @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use super::*; +use crate::telemetry::tests::{ + assert_deny_resolves, assert_no_deny, deny_spans, exported, span_attribute, +}; +use opentelemetry_proto::tonic::common::v1::any_value::Value; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn paired_credential_denial_shares_one_deny_span() { + let (events, received) = exported("warn", || { + emit_credential_endpoint_mismatch( + "POST", + "blocked.example", + 443, + "credential-policy", + &tracing::Span::none(), + ); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn repeated_credential_denials_keep_decision_spans_separate() { + for level in ["warn", "debug"] { + let (events, received) = exported(level, || { + let connect = + egress::connect_span(&EgressIntent::connect("blocked.example".to_string(), 443)); + // The FORWARD path passes its saved connect span without entering it. + for _ in 0..2 { + emit_credential_endpoint_mismatch( + "POST", + "blocked.example", + 443, + "credential-policy", + &connect, + ); + } + let allowed = NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(ActionId::Allowed) + .dst_endpoint(Endpoint::from_domain("allowed.example", 443)) + .build(); + egress_emit!(allowed); + }) + .await; + + assert_eq!(events.len(), 5); + assert_deny_resolves(&events[..4], &received, level == "warn"); + assert!(events[4].get("trace").is_none(), "deny context must exit"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 2); + assert_ne!(denies[0].span_id, denies[1].span_id); + assert_eq!(received.spans.len(), if level == "debug" { 3 } else { 2 }); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + for deny in denies { + assert_eq!(deny.trace_id, connect.trace_id); + assert_eq!(deny.parent_span_id, connect.span_id); + } + assert_eq!(events[0]["trace"]["uid"], events[2]["trace"]["uid"]); + } else { + assert_ne!(events[0]["trace"]["uid"], events[2]["trace"]["uid"]); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn network_class_forward_rejections_have_no_deny_trace() { + let (events, received) = exported("warn", || { + let Err(error) = + prepare_forward_target("/%zz", crate::l7::path::CanonicalizeOptions::default()) + else { + panic!("invalid escape must be rejected"); + }; + egress_emit!(build_forward_rejection_event( + "blocked.example", + 443, + format!("FORWARD rejecting non-canonical request-target: {error}") + )); + }) + .await; + assert_eq!(events.len(), 1); + assert_eq!(events[0]["action_id"], 2); + assert_no_deny(&events, &received); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn forward_denials_use_the_saved_connect_parent() { + for level in ["warn", "debug"] { + let (events, received) = crate::telemetry::tests::exported_over_otlp(level, || { + let intent = EgressIntent::connect("blocked.example".to_string(), 443); + let connect = egress::connect_span(&intent); + let decision = EgressDecision { + intent, + action: NetworkAction::Allow { + matched_policy: None, + }, + policy_generation: 1, + identity: ProcessIdentityEvidence::Available, + endpoint: EndpointDecision::default(), + binary: None, + binary_pid: None, + ancestors: vec![], + cmdline_paths: vec![], + }; + let denial = DestinationDenial { + kind: DestinationDenialKind::InternalAddress, + reason: "private destination".to_string(), + }; + let mut output = Vec::new(); + futures::executor::block_on(deny_forward_destination( + &mut output, + &connect, + &denial, + "127.0.0.1:41000".parse().unwrap(), + "POST", + "blocked.example", + 443, + "/", + "curl", + "42", + "-", + "curl", + "forward-policy", + &decision, + None, + None, + )) + .unwrap(); + assert!(output.starts_with(b"HTTP/1.1 403 Forbidden")); + }) + .await; + assert_eq!(events.len(), 1); + assert_deny_resolves(&events, &received, level == "warn"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 1); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + let deny = denies[0]; + assert_eq!(deny.parent_span_id, connect.span_id); + assert_eq!(events[0]["trace"]["uid"], hex::encode(&connect.trace_id)); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn forward_evaluation_error_keeps_the_http_decision_policy_context() { + for level in ["warn", "debug"] { + for mode in [ + crate::l7::EnforcementMode::Audit, + crate::l7::EnforcementMode::Enforce, + ] { + let enforced = mode == crate::l7::EnforcementMode::Enforce; + let (events, received) = exported(level, || { + let connect = egress::connect_span(&EgressIntent::forward_http( + "blocked.example".to_string(), + 443, + )); + let decision = HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .activity(ActivityId::Other) + .action(if enforced { + ActionId::Denied + } else { + ActionId::Allowed + }) + .disposition(if enforced { + DispositionId::Blocked + } else { + DispositionId::Allowed + }) + .http_request(HttpRequest::new( + "GET", + OcsfUrl::new("http", "blocked.example", "/", 443), + )) + .dst_endpoint(Endpoint::from_domain("blocked.example", 443)) + .firewall_rule("forward-policy", "l7") + .build(); + let error = build_forward_l7_evaluation_error( + "blocked.example", + 443, + mode, + "evaluation unavailable", + ); + // Use the same grouping as the forward-proxy production path. + emit_forward_l7_decision(decision, Some(error), &connect); + }) + .await; + + assert_eq!(events.len(), 2); + let http_decision = events + .iter() + .find(|event| event["class_uid"] == 4002) + .unwrap(); + let evaluation_error = events + .iter() + .find(|event| event["class_uid"] == 4001) + .unwrap(); + let denies = deny_spans(&received); + if enforced { + assert_eq!(denies.len(), 1); + assert_eq!(received.spans.len(), if level == "debug" { 2 } else { 1 }); + assert_eq!(http_decision["action_id"], 2); + assert_eq!(evaluation_error["action_id"], 2); + assert_deny_resolves(&events, &received, level == "warn"); + let attribute = |key: &str| span_attribute(denies[0], key); + assert_eq!( + attribute("openshell.policy.name"), + Some(&Value::StringValue("forward-policy".to_string())), + "the shared deny span must retain the HTTP decision's policy name" + ); + assert_eq!( + attribute("server.address"), + Some(&Value::StringValue("blocked.example".to_string())) + ); + assert_eq!(attribute("ocsf.class_uid"), Some(&Value::IntValue(4002))); + // This production decision currently has no status_detail. + assert!(attribute("openshell.policy.reason").is_none()); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + assert_eq!(denies[0].parent_span_id, connect.span_id); + assert_eq!(denies[0].trace_id, connect.trace_id); + } + } else { + assert_eq!(http_decision["action_id"], 1); + assert_eq!(evaluation_error["action_id"], 99); + assert_no_deny(&events, &received); + } + } + } +} diff --git a/crates/openshell-supervisor-network/src/run.rs b/crates/openshell-supervisor-network/src/run.rs index 9f8ce690f0..0c342c6bab 100644 --- a/crates/openshell-supervisor-network/src/run.rs +++ b/crates/openshell-supervisor-network/src/run.rs @@ -22,7 +22,7 @@ use openshell_core::policy::{NetworkMode, SandboxPolicy}; use openshell_core::proto::SandboxPolicy as ProtoSandboxPolicy; use openshell_core::provider_credentials::ProviderCredentialState; use openshell_ocsf::{ - ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, ocsf_emit, + ConfigStateChangeBuilder, SeverityId, StateId, StatusId, ctx::ctx as ocsf_ctx, }; use openshell_core::activity::ActivitySender; @@ -491,7 +491,7 @@ pub async fn run_networking( let upstream_config = build_upstream_client_config(&system_ca_bundle)?; let cert_cache = CertCache::new(ca); let state = Arc::new(ProxyTlsState::new(cert_cache, upstream_config)); - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::Informational) .status(StatusId::Success) @@ -510,7 +510,7 @@ pub async fn run_networking( // cannot rewrite credentials, so it fails closed on // TLS-bearing connections (see proxy.rs) rather than // leaking placeholders through a raw tunnel. - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) @@ -529,7 +529,7 @@ pub async fn run_networking( // rewrite credentials, so it fails closed on TLS-bearing // connections (see proxy.rs) rather than leaking placeholders // through a raw tunnel. - ocsf_emit!( + crate::telemetry::egress_emit!( ConfigStateChangeBuilder::new(ocsf_ctx()) .severity(SeverityId::High) .status(StatusId::Failure) diff --git a/crates/openshell-supervisor-network/src/telemetry.rs b/crates/openshell-supervisor-network/src/telemetry.rs new file mode 100644 index 0000000000..73e573b985 --- /dev/null +++ b/crates/openshell-supervisor-network/src/telemetry.rs @@ -0,0 +1,222 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Keep a deny span active during OCSF emission. + +use std::collections::HashMap; +use std::sync::{LazyLock, Mutex}; +use std::time::{Duration, Instant}; + +use openshell_ocsf::{ActionId, HttpActivityEvent, OcsfEvent}; + +/// Emit one egress event, entering an INFO deny span if it is denied HTTP Activity. +macro_rules! egress_emit { + ($event:expr) => { + $crate::telemetry::emit_decision([$event], None) + }; +} +pub(crate) use egress_emit; + +fn is_denied(event: &OcsfEvent) -> bool { + match event { + OcsfEvent::NetworkActivity(event) => event.action == Some(ActionId::Denied), + OcsfEvent::HttpActivity(event) => event.action == Some(ActionId::Denied), + OcsfEvent::DetectionFinding(event) => event.action == Some(ActionId::Denied), + _ => false, + } +} + +fn as_http(event: &OcsfEvent) -> Option<&HttpActivityEvent> { + match event { + OcsfEvent::HttpActivity(event) => Some(event), + _ => None, + } +} + +/// Emit all records of one synchronous decision under one deny span. Only HTTP +/// Activity carries `trace`, so only a denied decision with an HTTP record gets +/// a span. The span describes the denied HTTP record, or the first HTTP record +/// when only a companion finding is denied. A saved `parent` keeps the span in +/// the connect trace after authorization exits. +#[allow(clippy::disallowed_macros)] +pub fn emit_decision(events: impl IntoIterator, parent: Option<&tracing::Span>) { + let events: Vec<_> = events.into_iter().collect(); + let span = if events.iter().any(is_denied) { + events + .iter() + .filter(|event| is_denied(event)) + .find_map(as_http) + .or_else(|| events.iter().find_map(as_http)) + .and_then(|event| deny_span(event, parent)) + } else { + None + }; + let emit = || { + for event in events { + openshell_ocsf::ocsf_emit!(event); + } + }; + if let Some(span) = span { + span.in_scope(emit); + } else { + emit(); + } +} + +/// Returns `None` when deny spans are disabled or the destination has used up +/// its deny spans. Disabled spans spend no tokens. +fn deny_span(event: &HttpActivityEvent, parent: Option<&tracing::Span>) -> Option { + if !tracing::enabled!(kind: tracing::metadata::Kind::SPAN, tracing::Level::INFO) { + return None; + } + let endpoint = event.dst_endpoint.as_ref(); + let address = + endpoint.and_then(|endpoint| endpoint.domain.as_deref().or(endpoint.ip.as_deref())); + let port = endpoint.and_then(|endpoint| endpoint.port); + let destination = format!( + "{}:{}", + address.unwrap_or_default(), + port.unwrap_or_default() + ); + if !LIMITER + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .allow(&destination, Instant::now()) + { + return None; + } + let parent = parent.cloned().unwrap_or_else(tracing::Span::current); + Some(tracing::info_span!( + parent: &parent, + "supervisor.egress.deny", + openshell.policy.decision = "deny", + openshell.policy.name = event.firewall_rule.as_ref().map(|rule| rule.name.as_str()), + openshell.policy.reason = event.base.status_detail.as_deref(), + server.address = address, + server.port = port.map(i64::from), + ocsf.class_uid = i64::from(event.base.class_uid), + )) +} + +/// Deny spans one destination can export at once; one more is allowed per +/// second after that. +const DENY_SPAN_BURST: u32 = 10; +const DENY_SPAN_REFILL: Duration = Duration::from_secs(1); +const DENY_SPAN_DESTINATIONS: usize = 1024; +/// Bound aggregate span creation even when a workload rotates destinations. +const DENY_SPAN_GLOBAL_BURST: u32 = 100; +const DENY_SPAN_GLOBAL_REFILL: Duration = Duration::from_millis(10); + +static LIMITER: LazyLock> = LazyLock::new(|| Mutex::new(default_limiter())); + +fn default_limiter() -> DenySpanLimiter { + DenySpanLimiter::new(DENY_SPAN_BURST, DENY_SPAN_REFILL, DENY_SPAN_DESTINATIONS) +} + +#[cfg(test)] +fn reset_deny_span_limiter() { + *LIMITER + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = default_limiter(); +} + +/// Per-destination and process-wide token buckets bounding deny-span work. +/// OCSF records are unaffected; a limited decision creates no additional span. +struct DenySpanLimiter { + burst: u32, + refill: Duration, + capacity: usize, + buckets: HashMap, + global_burst: u32, + global_refill: Duration, + global: Option, +} + +struct Bucket { + tokens: u32, + refilled: Instant, +} + +impl DenySpanLimiter { + fn new(burst: u32, refill: Duration, capacity: usize) -> Self { + Self::with_global_limit( + burst, + refill, + capacity, + DENY_SPAN_GLOBAL_BURST, + DENY_SPAN_GLOBAL_REFILL, + ) + } + + fn with_global_limit( + burst: u32, + refill: Duration, + capacity: usize, + global_burst: u32, + global_refill: Duration, + ) -> Self { + Self { + burst, + refill, + capacity, + buckets: HashMap::new(), + global_burst, + global_refill, + global: None, + } + } + + fn allow(&mut self, destination: &str, now: Instant) -> bool { + let global = self.global.get_or_insert(Bucket { + tokens: self.global_burst, + refilled: now, + }); + global.refill(self.global_burst, self.global_refill, now); + if global.tokens == 0 { + return false; + } + if !self.buckets.contains_key(destination) && self.buckets.len() >= self.capacity { + for bucket in self.buckets.values_mut() { + bucket.refill(self.burst, self.refill, now); + } + let burst = self.burst; + self.buckets.retain(|_, bucket| bucket.tokens < burst); + if self.buckets.len() >= self.capacity { + return false; + } + } + let bucket = self + .buckets + .entry(destination.to_string()) + .or_insert(Bucket { + tokens: self.burst, + refilled: now, + }); + bucket.refill(self.burst, self.refill, now); + let allowed = bucket.tokens > 0; + if allowed { + bucket.tokens -= 1; + global.tokens -= 1; + } + allowed + } +} + +impl Bucket { + fn refill(&mut self, burst: u32, interval: Duration, now: Instant) { + let earned = now.saturating_duration_since(self.refilled).as_nanos() / interval.as_nanos(); + if earned == 0 { + return; + } + let earned = u32::try_from(earned).unwrap_or(u32::MAX); + self.tokens = self.tokens.saturating_add(earned).min(burst); + self.refilled = if self.tokens == burst { + now + } else { + self.refilled + interval * earned + }; + } +} + +#[cfg(test)] +pub mod tests; diff --git a/crates/openshell-supervisor-network/src/telemetry/tests.rs b/crates/openshell-supervisor-network/src/telemetry/tests.rs new file mode 100644 index 0000000000..7a3388ad0d --- /dev/null +++ b/crates/openshell-supervisor-network/src/telemetry/tests.rs @@ -0,0 +1,602 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{Arc, LazyLock, Mutex}; + +use openshell_otel_test_support::{OtlpTestServer, ReceivedTraces, tracing_test_lock}; +use opentelemetry_proto::tonic::common::v1::any_value::Value; +use opentelemetry_proto::tonic::trace::v1::Span; +use tracing_subscriber::prelude::*; + +#[derive(Clone)] +struct Writer(Arc>>); +impl std::io::Write for Writer { + fn write(&mut self, bytes: &[u8]) -> std::io::Result { + self.0.lock().unwrap().extend_from_slice(bytes); + Ok(bytes.len()) + } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } +} + +/// Capture the actual OCSF dispatch and the spans an in-memory exporter +/// receives through the supervisor's span filter. +pub async fn exported( + log_level: &str, + emit: impl FnOnce(), +) -> (Vec, ReceivedTraces) { + let _lock = tracing_test_lock().await; + LazyLock::force(®ISTER); + super::reset_deny_span_limiter(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporter::default(); + let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let events = capture(log_level, &provider, emit); + provider.force_flush().unwrap(); + let spans = exporter + .get_finished_spans() + .unwrap() + .into_iter() + .map(Into::into) + .collect(); + provider.shutdown().unwrap(); + ( + events, + ReceivedTraces { + spans, + ..ReceivedTraces::default() + }, + ) +} + +/// Capture the actual OCSF dispatch and receive spans over OTLP/gRPC. +pub async fn exported_over_otlp( + log_level: &str, + emit: impl FnOnce(), +) -> (Vec, ReceivedTraces) { + let _lock = tracing_test_lock().await; + LazyLock::force(®ISTER); + super::reset_deny_span_limiter(); + let collector = OtlpTestServer::start().await; + let (provider, error) = openshell_otel::provider_for(Some(openshell_otel::OtlpExportConfig { + endpoint: collector.endpoint(), + service_name: openshell_otel::ServiceName::Fixed("openshell-supervisor"), + service_version: None, + resource_attributes: vec![], + })); + assert!(error.is_none()); + let provider = provider.unwrap(); + let events = capture(log_level, &provider, emit); + provider.force_flush().unwrap(); + collector.wait_for_export().await; + provider.shutdown().unwrap(); + (events, collector.shutdown().await) +} + +static REGISTER: LazyLock<()> = LazyLock::new(|| { + assert!(openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation + )); +}); + +fn capture( + log_level: &str, + provider: &opentelemetry_sdk::trace::SdkTracerProvider, + emit: impl FnOnce(), +) -> Vec { + let bytes = Arc::new(Mutex::new(vec![])); + let subscriber = tracing_subscriber::registry() + .with( + openshell_ocsf::OcsfJsonlLayer::new(Writer(bytes.clone())) + .with_filter(tracing_subscriber::filter::LevelFilter::INFO), + ) + .with( + openshell_otel::layer(provider, "openshell-supervisor") + .with_filter(openshell_otel::supervisor_span_filter(log_level)), + ) + // The shared test registry keeps callsites enabled process-wide. + // Gate this subscriber too, so the default case also checks a disabled + // DEBUG connect handle, as well as absence from the OTLP export. + .with(openshell_otel::supervisor_span_filter(log_level)); + tracing::subscriber::with_default(subscriber, emit); + String::from_utf8(bytes.lock().unwrap().clone()) + .unwrap() + .lines() + .map(|line| serde_json::from_str(line).unwrap()) + .collect() +} + +pub fn assert_deny_resolves(events: &[serde_json::Value], received: &ReceivedTraces, root: bool) { + assert!(events.iter().any(|event| event["class_uid"] == 4002)); + for event in events { + if event["class_uid"] != 4002 { + assert!( + event.get("trace").is_none(), + "only HTTP Activity carries trace" + ); + continue; + } + let uid = event["trace"]["uid"] + .as_str() + .expect("emission was inside sampled deny span"); + assert_eq!(uid.len(), 32); + assert!( + event["metadata"]["profiles"] + .as_array() + .unwrap() + .iter() + .any(|p| p == "trace") + ); + let span = deny_spans(received) + .into_iter() + .find(|span| hex::encode(&span.trace_id) == uid) + .expect("OCSF trace.uid resolves to exported deny span"); + if root { + assert!(span.parent_span_id.is_empty()); + assert_eq!( + received + .spans + .iter() + .filter(|other| other.trace_id == span.trace_id) + .count(), + 1 + ); + } + let attributes = span + .attributes + .iter() + .map(|kv| kv.key.as_str()) + .collect::>(); + for key in ["openshell.policy.decision", "ocsf.class_uid"] { + assert!(attributes.contains(&key), "deny span carries {key}"); + } + let class_uid = span_attribute(span, "ocsf.class_uid"); + assert!( + matches!(class_uid, Some(Value::IntValue(_))), + "deny span exports ocsf.class_uid as an integer: {class_uid:?}" + ); + } +} + +pub fn http_deny(host: &str, decision: &str) -> openshell_ocsf::OcsfEvent { + openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .http_request(openshell_ocsf::HttpRequest::new( + "GET", + openshell_ocsf::Url::new("https", host, "/", 443), + )) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain(host, 443)) + .status_detail(decision) + .message(decision) + .build() +} + +pub fn deny_spans(received: &ReceivedTraces) -> Vec<&Span> { + received + .spans + .iter() + .filter(|span| span.name == "supervisor.egress.deny") + .collect() +} + +/// Assert that no record carries `trace` and no deny span was exported. +pub fn assert_no_deny(events: &[serde_json::Value], received: &ReceivedTraces) { + for event in events { + assert!(event.get("trace").is_none(), "{event}"); + } + assert!(deny_spans(received).is_empty()); +} + +pub fn span_attribute<'a>(span: &'a Span, key: &str) -> Option<&'a Value> { + span.attributes + .iter() + .find(|attribute| attribute.key == key) + .and_then(|attribute| attribute.value.as_ref()?.value.as_ref()) +} + +pub fn assert_deny_attribute(received: &ReceivedTraces, key: &str, expected: &str) { + assert_eq!( + span_attribute(deny_spans(received)[0], key), + Some(&Value::StringValue(expected.to_string())) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn companion_records_share_one_deny_span() { + let (events, received) = exported("warn", || { + let event = http_deny("blocked.example", "denied"); + let finding = + openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()).build(); + super::emit_decision([event, finding], None); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(received.spans.len(), 1); + assert_deny_resolves(&events, &received, true); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn concurrent_decisions_keep_companions_and_request_contexts_separate() { + for level in ["warn", "debug"] { + let (events, received) = exported(level, || { + let dispatch = tracing::dispatcher::get_default(Clone::clone); + let barrier = std::sync::Barrier::new(2); + std::thread::scope(|threads| { + for host in ["alpha.example", "beta.example"] { + let dispatch = &dispatch; + let barrier = &barrier; + threads.spawn(move || { + tracing::dispatcher::with_default(dispatch, || { + let outside = openshell_ocsf::ctx::ctx().sandbox_id.clone(); + let mut context = (*openshell_ocsf::ctx::ctx()).clone(); + context.sandbox_id = host.to_string(); + context.sandbox_name = format!("sandbox-{host}"); + openshell_ocsf::ctx::bind_thread_ctx(Some(Arc::new(context))); + let connect = tracing::debug_span!("supervisor.egress.connect"); + connect.in_scope(|| { + for attempt in 0..2 { + let decision = format!("{host}:{attempt}"); + let event = http_deny(host, &decision); + let finding = openshell_ocsf::DetectionFindingBuilder::new( + openshell_ocsf::ctx::ctx(), + ) + .message(&decision) + .build(); + super::emit_decision([event, finding], None); + // Both threads finish each decision before either + // starts the next. + barrier.wait(); + } + }); + openshell_ocsf::ctx::bind_thread_ctx(None); + assert_eq!(openshell_ocsf::ctx::ctx().sandbox_id, outside); + }); + }); + } + }); + }) + .await; + + assert_eq!(events.len(), 8); + assert_deny_resolves(&events, &received, level == "warn"); + let denies = deny_spans(&received); + assert_eq!(denies.len(), 4); + assert_eq!(received.spans.len(), if level == "debug" { 6 } else { 4 }); + let attribute = |span: &Span, key: &str| match span_attribute(span, key) { + Some(Value::StringValue(value)) => value.clone(), + other => panic!("expected string attribute {key}, got {other:?}"), + }; + let mut traces_by_host = Vec::new(); + for host in ["alpha.example", "beta.example"] { + let mut traces = Vec::new(); + for attempt in 0..2 { + let decision = format!("{host}:{attempt}"); + let pair = events + .iter() + .filter(|event| event["message"] == decision) + .collect::>(); + assert_eq!(pair.len(), 2); + for event in &pair { + assert_eq!(event["device"]["container"]["uid"], host); + assert_eq!( + event["device"]["container"]["name"], + format!("sandbox-{host}") + ); + } + let http = pair + .iter() + .find(|event| event["class_uid"] == 4002) + .unwrap(); + let spans = denies + .iter() + .filter(|span| attribute(span, "openshell.policy.reason") == decision) + .collect::>(); + assert_eq!(spans.len(), 1); + let deny = spans[0]; + assert_eq!(attribute(deny, "server.address"), host); + assert_eq!(http["trace"]["uid"], hex::encode(&deny.trace_id)); + if level == "debug" { + let connect = received + .spans + .iter() + .find(|span| span.span_id == deny.parent_span_id) + .unwrap(); + assert_eq!(connect.name, "supervisor.egress.connect"); + assert_eq!(connect.trace_id, deny.trace_id); + } + traces.push(deny.trace_id.clone()); + } + if level == "debug" { + assert_eq!(traces[0], traces[1]); + } else { + assert_ne!(traces[0], traces[1]); + } + traces_by_host.push(traces); + } + for alpha in &traces_by_host[0] { + for beta in &traces_by_host[1] { + assert_ne!( + alpha, beta, + "independent request contexts must stay separate" + ); + } + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn deny_span_describes_the_denied_http_record() { + let (events, received) = exported("warn", || { + let allowed = openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Allowed) + .http_request(openshell_ocsf::HttpRequest::new( + "GET", + openshell_ocsf::Url::new("https", "allowed.example", "/", 443), + )) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "allowed.example", + 443, + )) + .build(); + super::emit_decision([allowed, http_deny("blocked.example", "denied")], None); + }) + .await; + assert_deny_resolves(&events, &received, true); + assert_deny_attribute(&received, "server.address", "blocked.example"); + assert_deny_attribute(&received, "openshell.policy.reason", "denied"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn missing_deny_context_leaves_optional_attributes_unset() { + let (events, received) = exported("warn", || { + let http = openshell_ocsf::HttpActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .http_request(openshell_ocsf::HttpRequest { + http_method: "GET".parse().unwrap(), + url: None, + }) + .message("diagnostic text is not a policy reason") + .build(); + super::egress_emit!(http); + }) + .await; + assert_deny_resolves(&events, &received, true); + assert_eq!(events.len(), 1); + assert_eq!(received.spans.len(), 1); + for span in &received.spans { + for key in [ + "openshell.policy.name", + "openshell.policy.reason", + "server.address", + "server.port", + ] { + assert!( + span.attributes.iter().all(|kv| kv.key != key), + "{key} stays unset" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn denials_without_an_http_record_do_not_open_deny_spans() { + let (events, received) = exported("warn", || { + let network = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "blocked.example", + 443, + )) + .build(); + let finding = openshell_ocsf::DetectionFindingBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Denied) + .build(); + super::emit_decision([network, finding], None); + }) + .await; + assert_eq!(events.len(), 2); + assert_no_deny(&events, &received); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn allowed_emissions_do_not_create_info_deny_spans() { + let (events, received) = exported("warn", || { + let allowed = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) + .action(openshell_ocsf::ActionId::Allowed) + .dst_endpoint(openshell_ocsf::Endpoint::from_domain( + "allowed.example", + 443, + )) + .build(); + super::egress_emit!(allowed); + crate::l7::middleware::emit_middleware_uninspectable( + &crate::l7::relay::L7EvalContext::default(), + "unknown protocol", + false, + ); + }) + .await; + assert_eq!(events.len(), 2); + assert_no_deny(&events, &received); +} + +#[test] +fn deny_span_limiter_bounds_each_destination_and_refills() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::new(2, Duration::from_secs(1), 8); + assert!(limiter.allow("a:443", start)); + assert!(limiter.allow("a:443", start)); + assert!(!limiter.allow("a:443", start)); + assert!( + limiter.allow("b:443", start), + "destinations are independent" + ); + assert!(!limiter.allow("a:443", start + Duration::from_millis(999))); + assert!(limiter.allow("a:443", start + Duration::from_secs(1))); + assert!(!limiter.allow("a:443", start + Duration::from_secs(1))); + let later = start + Duration::from_mins(1); + assert!(limiter.allow("a:443", later)); + assert!(limiter.allow("a:443", later)); + assert!(!limiter.allow("a:443", later), "refill stops at the burst"); +} + +#[test] +fn deny_span_limiter_bounds_tracked_destinations() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::new(1, Duration::from_secs(1), 2); + assert!(limiter.allow("a", start)); + assert!(limiter.allow("b", start)); + assert!( + !limiter.allow("c", start), + "untracked destinations wait for room" + ); + // Full buckets are idle and can be evicted. + assert!(limiter.allow("c", start + Duration::from_secs(1))); +} + +#[test] +fn deny_span_limiter_bounds_aggregate_bursts_and_refills() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::with_global_limit( + 1, + Duration::from_secs(1), + 8, + 2, + Duration::from_millis(100), + ); + assert!(limiter.allow("a", start)); + assert!( + !limiter.allow("a", start), + "a destination rejection spends no global token" + ); + assert!(limiter.allow("b", start)); + assert!( + !limiter.allow("c", start), + "rotating destinations cannot exceed the global burst" + ); + assert!(!limiter.allow("c", start + Duration::from_millis(99))); + assert!(limiter.allow("c", start + Duration::from_millis(100))); + assert!(!limiter.allow("d", start + Duration::from_millis(100))); + let later = start + Duration::from_secs(10); + assert!(limiter.allow("a", later)); + assert!(limiter.allow("b", later)); + assert!( + !limiter.allow("c", later), + "global refill stops at its burst" + ); +} + +#[test] +fn destination_capacity_rejections_spend_no_global_tokens() { + use std::time::{Duration, Instant}; + + let start = Instant::now(); + let mut limiter = super::DenySpanLimiter::with_global_limit( + 10, + Duration::from_secs(1), + 1, + 2, + Duration::from_secs(1), + ); + assert!(limiter.allow("a", start)); + assert!(!limiter.allow("b", start)); + assert!(limiter.allow("a", start)); + assert!(!limiter.allow("a", start)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn aggregate_deny_limit_preserves_every_audit_record() { + let (events, received) = exported("warn", || { + *super::LIMITER.lock().unwrap() = super::DenySpanLimiter::with_global_limit( + 10, + std::time::Duration::from_secs(1), + 1024, + 3, + std::time::Duration::MAX, + ); + for index in 0..12 { + super::egress_emit!(http_deny(&format!("host-{index}.example"), "denied")); + } + }) + .await; + assert_eq!( + events.len(), + 12, + "the span budget never suppresses audit records" + ); + assert!(events.iter().all(|event| event["action_id"] == 2)); + assert_eq!(deny_spans(&received).len(), 3); + assert_eq!( + events + .iter() + .filter(|event| event.get("trace").is_some()) + .count(), + 3 + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn limited_denial_keeps_an_active_sampled_connect_trace() { + let (events, received) = exported("debug", || { + *super::LIMITER.lock().unwrap() = super::DenySpanLimiter::with_global_limit( + 1, + std::time::Duration::MAX, + 1024, + 100, + std::time::Duration::MAX, + ); + tracing::debug_span!("supervisor.egress.connect").in_scope(|| { + super::egress_emit!(http_deny("blocked.example", "first")); + super::egress_emit!(http_deny("blocked.example", "limited")); + }); + }) + .await; + assert_eq!(events.len(), 2); + assert_eq!(deny_spans(&received).len(), 1); + let connect = received + .spans + .iter() + .find(|span| span.name == "supervisor.egress.connect") + .unwrap(); + assert_eq!(events[1]["trace"]["uid"], hex::encode(&connect.trace_id)); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn repeated_denials_to_one_destination_export_bounded_spans() { + let (events, received) = exported("warn", || { + for _ in 0..(super::DENY_SPAN_BURST + 5) { + super::egress_emit!(http_deny("loop.example", "denied")); + } + super::egress_emit!(http_deny("other.example", "denied")); + }) + .await; + let denies = deny_spans(&received).len(); + assert_eq!(denies, super::DENY_SPAN_BURST as usize + 1); + let traced = events + .iter() + .filter(|event| event.get("trace").is_some()) + .count(); + assert_eq!(traced, denies, "limited records carry no trace.uid"); + assert_eq!(events.len(), super::DENY_SPAN_BURST as usize + 6); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn disabled_deny_spans_spend_no_tokens() { + let _lock = tracing_test_lock().await; + super::reset_deny_span_limiter(); + let subscriber = + tracing_subscriber::registry().with(tracing_subscriber::filter::LevelFilter::WARN); + tracing::subscriber::with_default(subscriber, || { + super::egress_emit!(http_deny("quiet.example", "denied")); + }); + assert!(super::LIMITER.lock().unwrap().buckets.is_empty()); + assert!(super::LIMITER.lock().unwrap().global.is_none()); +} diff --git a/crates/openshell-supervisor/src/main.rs b/crates/openshell-supervisor/src/main.rs index feb3aea9c8..4c1bcac7a0 100644 --- a/crates/openshell-supervisor/src/main.rs +++ b/crates/openshell-supervisor/src/main.rs @@ -18,7 +18,6 @@ use openshell_core::sandbox_env::OtlpProtocol; use openshell_isolation_interface::contract::BackendDescriptor; use tracing::{error, info, warn}; use tracing_subscriber::EnvFilter; -use tracing_subscriber::filter::LevelFilter; use tracing_subscriber::{Layer as _, layer::SubscriberExt as _, util::SubscriberInitExt as _}; mod logging; @@ -455,6 +454,10 @@ fn main() -> Result<()> { return openshell_supervisor::check_control_readiness(&args.socket); } + openshell_ocsf::register_trace_correlation_extractor( + openshell_otel::current_ocsf_trace_correlation, + ); + let args = Args::parse(); if args.mode == SupervisorMode::Shared { return shared::run(args); @@ -563,7 +566,7 @@ fn main() -> Result<()> { otlp_layer_provider .as_ref() .map(|provider| openshell_otel::layer(provider, SERVICE_NAME)) - .with_filter(otlp_span_filter(&args.log_level)), + .with_filter(openshell_otel::supervisor_span_filter(&args.log_level)), ) .init(); if !file_available { @@ -845,16 +848,6 @@ fn supervisor_resource_attributes( .collect() } -/// A more verbose log level raises `openshell*` spans; dependency spans stay at -/// INFO so they do not flood the exporter queue. -fn otlp_span_filter(log_level: &str) -> EnvFilter { - let level = log_level - .parse::() - .unwrap_or(LevelFilter::INFO) - .max(LevelFilter::INFO); - EnvFilter::new(format!("info,openshell={level}")) -} - /// Choose the supervisor's exit status for a sandbox run. /// /// The reserved status tells the compute driver that the sandbox rejected the @@ -957,15 +950,6 @@ mod tests { use super::*; - #[test] - fn otlp_span_filter_raises_only_openshell_targets() { - let directives = |log_level: &str| otlp_span_filter(log_level).to_string(); - assert_eq!(directives("warn"), "openshell=info,info"); - assert_eq!(directives("info"), "openshell=info,info"); - assert_eq!(directives("debug"), "openshell=debug,info"); - assert_eq!(directives("info,h2=debug"), "openshell=info,info"); - } - #[test] fn only_workspace_validation_failure_exits_with_reserved_status() { use miette::WrapErr as _; diff --git a/docs/how-it-works/gateways/configuration.mdx b/docs/how-it-works/gateways/configuration.mdx index 175836ccb4..424efa6016 100644 --- a/docs/how-it-works/gateways/configuration.mdx +++ b/docs/how-it-works/gateways/configuration.mdx @@ -810,6 +810,7 @@ Each OCSF event becomes one log record: | Timestamp | The OCSF `time`. The observed timestamp is when the gateway received the event. | | Severity | Unknown, Informational, and Other are INFO, Low is WARN, Medium is WARN2, High is ERROR, Critical is ERROR2, and Fatal is FATAL. The severity text is the OCSF severity name. | | Attributes | `ocsf.class_uid`, `ocsf.activity_id`, `ocsf.severity_id`, `ocsf.metadata.uid`, and, for events associated with a sandbox, `openshell.sandbox.id`. | +| Trace context | The trace and span ID of the sampled span active when the event was emitted, for every OCSF class. Absent when no sampled span was active. | `schema_version` in this table is deprecated and ignored; the gateway logs a warning at startup when it is set. diff --git a/docs/observability/ocsf-json-export.mdx b/docs/observability/ocsf-json-export.mdx index e5cc5e363b..2f4b52c10f 100644 --- a/docs/observability/ocsf-json-export.mdx +++ b/docs/observability/ocsf-json-export.mdx @@ -286,6 +286,37 @@ And a denied connection, produced when `curl https://github.com` runs in a sandb The JSON examples above are formatted for readability. The actual JSONL file contains one JSON object per line with no whitespace formatting. +## Refusal Records + +Every record that describes a refusal sets `action` to `Denied`. On Network and HTTP Activity, `disposition` is `Blocked`, except for FORWARD requests with an unsupported scheme, which report `Rejected`. Besides policy denials, this includes: + +- FORWARD requests the proxy rejects as unparsable, non-canonical, or invalid GraphQL or JSON-RPC. +- L7 evaluation errors under `enforcement: enforce`. Under `enforcement: audit` they report `action` Other and `disposition` Allowed, because the request proceeds. +- Google Cloud metadata server emulation responses with status 403 or 405. +- Fail-closed request, response, and WebSocket middleware, including the HTTP Activity record for a fail-closed response stage and the Detection Findings for a fail-closed stage or chain or a failed post-middleware policy evaluation. +- Response delivery the supervisor refuses because middleware stopped it, its preflight failed, or the upstream response head could not be normalized for it. + +Count these records when you track denials, for example with `select(.action == "Denied")`. + +## Follow a Deny Trace + +HTTP Activity and API Activity records emitted inside a sampled OpenTelemetry span carry `trace.uid` and list `trace` in `metadata.profiles`. These are the classes OCSF 1.8.0 declares the Trace profile for. The UID is the W3C trace ID, 32 lowercase hexadecimal characters. Search for it in your Jaeger or Tempo backend. A sampled ID does not guarantee export or retention in the trace backend. + +```json +{ + "metadata": { "profiles": ["container", "host", "network_proxy", "security_control", "trace"] }, + "trace": { "uid": "0af7651916cd43dd8448eb211c80319c" } +} +``` + +OTLP log export links every OCSF record, whatever its class, to the active sampled span through the log record's own trace and span ID fields. Use those fields for log-to-trace links in your backend. + +With OTLP trace export enabled on a dedicated supervisor, denied HTTP requests can create an INFO `supervisor.egress.deny` span around the audit records for that decision. The span records the policy, reason, and destination when available. At the default log level, every deny span is a root span. At debug level, FORWARD policy, destination, credential-binding, and request middleware denials can join the available connect trace and its authorization, resolve, and dial spans. Other deny spans, including L7 denials inside CONNECT tunnels and response middleware denials, remain root spans. Shared supervisor hosts do not initialize OTLP exporters, so their records carry no trace correlation. Workload `traceparent` headers never supply supervisor correlation. + +To bound telemetry volume when a workload retries blocked destinations, the supervisor creates at most 10 deny spans per destination at once and one more per second after that. A process-wide budget also allows at most 100 spans at once and refills at 100 per second across destinations. Rate-limited decisions create no additional deny span, while all audit records are still emitted. Records can retain correlation to an already active sampled span, including a connect span at debug level. + +Gateway records and their OTLP trace context can inherit the caller's incoming `traceparent`, including before authentication succeeds. The caller also decides whether they are correlated at all, because an unsampled `traceparent` suppresses `trace.uid`. Treat gateway trace IDs as correlation hints, not evidence of the caller's identity, and do not treat a missing `trace.uid` as meaningful. + ## OCSF Event Classes in JSON The `class_uid` field identifies the event type: From 11ff9459452fef21d51702c99fb25b7a8a554a31 Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Fri, 6 Nov 2026 16:43:55 +0000 Subject: [PATCH 19/31] test(conformance): ignore slow-TERM delete until the supervisor half of #5025 lands (#5338) The test fails on Podman because the supervisor container waits out its stop timeout. Also list ignored tests in check-conformance-program.sh so the program list still matches nextest's. Signed-off-by: Evan Lezar --- tasks/scripts/check-conformance-program.sh | 3 ++- tests/suites/conformance/src/lifecycle.rs | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tasks/scripts/check-conformance-program.sh b/tasks/scripts/check-conformance-program.sh index 7bfb2ac8c4..6d054405f7 100755 --- a/tasks/scripts/check-conformance-program.sh +++ b/tasks/scripts/check-conformance-program.sh @@ -16,7 +16,8 @@ trap 'rm -rf "${work}"' EXIT cargo run --quiet --locked --manifest-path "${manifest}" --bin openshell-conformance -- list \ | awk '{print $1}' | sort > "${work}/program" -cargo nextest list --locked --color never --manifest-path "${manifest}" --message-format oneline \ +# --run-ignored all: the program lists ignored tests too, so nextest must as well. +cargo nextest list --locked --color never --run-ignored all --manifest-path "${manifest}" --message-format oneline \ | awk '{print $2}' | sort > "${work}/nextest" if [[ ! -s "${work}/nextest" ]]; then diff --git a/tests/suites/conformance/src/lifecycle.rs b/tests/suites/conformance/src/lifecycle.rs index 6af1d145b3..dd47c0b4d6 100644 --- a/tests/suites/conformance/src/lifecycle.rs +++ b/tests/suites/conformance/src/lifecycle.rs @@ -52,7 +52,11 @@ const DELETE_DEADLINE: Duration = Duration::from_secs(10); /// Many workloads do not exit immediately on TERM, for example while they run /// cleanup handlers. Deleting such a running sandbox should still complete /// promptly, rather than waiting out the runtime's termination grace period. +// +// Unignore this when the supervisor half of #5025 lands: on Podman the +// supervisor container still waits out its stop timeout. #[testkit::test] +#[ignore = "https://github.com/NVIDIA/OpenShell/issues/5025"] async fn delete_is_prompt_when_workload_is_slow_to_handle_term( cx: &mut Context, ) -> testkit::Result { From c162ccc2cc58dfc9b371401e06ebb1785df65559 Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:21:30 +0000 Subject: [PATCH 20/31] fix(sandbox): validate filesystem policy anchors (#5278) * fix(sandbox): validate filesystem policy anchors Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> * fix(ssh): keep startup failures from stalling sessions Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --------- Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-sandbox/README.md | 7 + .../src/sandbox/linux/landlock.rs | 381 +++++++++++++++--- .../openshell-supervisor-process/src/ssh.rs | 209 +++++++++- .../src/ssh/exec_input_tests.rs | 95 ++++- docs/how-it-works/policies/schema.mdx | 9 +- e2e/rust/tests/landlock.rs | 76 ++++ skills/generate-sandbox-policy/SKILL.md | 4 + 7 files changed, 702 insertions(+), 79 deletions(-) diff --git a/crates/openshell-sandbox/README.md b/crates/openshell-sandbox/README.md index 72d01b73c7..e365721b8e 100644 --- a/crates/openshell-sandbox/README.md +++ b/crates/openshell-sandbox/README.md @@ -10,6 +10,13 @@ the lower-level mechanisms used by that orchestration and the boundary server. Unsafe kernel ABI, process-launch, and descriptor-ownership operations stay in this runtime crate behind safe APIs where their contracts can be enforced. +Landlock policy anchors are resolved relative to open directory descriptors. +Symlinks are followed only through ancestors that the workload neither owns nor +can write, preserving system aliases while rejecting mutable workspace links. +The rule uses the resolved descriptor directly. Unsafe resolution and an empty +effective allowlist fail the launch independently of Landlock compatibility; +they cannot trigger the best-effort fallback to baseline-only restrictions. + The network broker retains source socket descriptors only while preparing a workload endpoint. A successful connection or `listen` releases that reference; the kernel then tracks the lifetime through workload-owned descriptors. Native diff --git a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs index 25b505f389..b695ccfc82 100644 --- a/crates/openshell-sandbox/src/sandbox/linux/landlock.rs +++ b/crates/openshell-sandbox/src/sandbox/linux/landlock.rs @@ -3,13 +3,19 @@ //! Landlock filesystem sandboxing. +#[cfg(test)] +use landlock::PathFd; use landlock::{ - ABI, Access, AccessFs, BitFlags, CompatLevel, Compatible, PathBeneath, PathFd, PathFdError, - Ruleset, RulesetAttr, RulesetCreatedAttr, + ABI, Access, AccessFs, BitFlags, CompatLevel, Compatible, PathBeneath, Ruleset, RulesetAttr, + RulesetCreatedAttr, }; use miette::{IntoDiagnostic, Result}; use openshell_core::policy::{LandlockCompatibility, SandboxPolicy}; +use std::collections::VecDeque; +use std::ffi::OsString; use std::os::fd::{AsFd, OwnedFd}; +use std::os::unix::ffi::OsStringExt; +use std::path::Component; use std::path::{Path, PathBuf}; use tracing::debug; @@ -318,6 +324,19 @@ fn prepare_with_path_open_mode( .build() ); + // Path admission is independent of kernel compatibility. In particular, an + // unsafe policy anchor must never reach the best-effort ruleset fallback. + let read_only = open_policy_paths(&read_only, compatibility, path_open_mode)?; + let read_write = open_policy_paths(&read_write, compatibility, path_open_mode)?; + let rules_applied = read_only.len() + read_write.len(); + if rules_applied == 0 { + return Err(miette::miette!( + "Landlock ruleset has zero valid paths — all {} path(s) failed to open. \ + Refusing to continue without filesystem restrictions.", + total_paths, + )); + } + let result: Result = (|| { let access_all = AccessFs::from_all(abi); let access_read = AccessFs::from_read(abi); @@ -329,36 +348,20 @@ fn prepare_with_path_open_mode( .into_diagnostic()?; let mut ruleset = ruleset.create().into_diagnostic()?; - let mut rules_applied: usize = 0; - - for path in &read_only { - if let Some(path_fd) = try_open_path(path, compatibility, path_open_mode)? { - let allowed_access = access_for_path_fd(&path_fd, access_read, abi)?; - debug!(path = %path.display(), "Landlock allow read-only"); - ruleset = ruleset - .add_rule(PathBeneath::new(path_fd, allowed_access)) - .into_diagnostic()?; - rules_applied += 1; - } - } - - for path in &read_write { - if let Some(path_fd) = try_open_path(path, compatibility, path_open_mode)? { - let allowed_access = access_for_path_fd(&path_fd, access_all, abi)?; - debug!(path = %path.display(), "Landlock allow read-write"); - ruleset = ruleset - .add_rule(PathBeneath::new(path_fd, allowed_access)) - .into_diagnostic()?; - rules_applied += 1; - } + for (path, path_fd) in read_only { + let allowed_access = access_for_path_fd(&path_fd, access_read, abi)?; + debug!(path = %path.display(), "Landlock allow read-only"); + ruleset = ruleset + .add_rule(PathBeneath::new(path_fd, allowed_access)) + .into_diagnostic()?; } - if rules_applied == 0 { - return Err(miette::miette!( - "Landlock ruleset has zero valid paths — all {} path(s) failed to open. \ - Refusing to apply an empty ruleset that would block all filesystem access.", - total_paths, - )); + for (path, path_fd) in read_write { + let allowed_access = access_for_path_fd(&path_fd, access_all, abi)?; + debug!(path = %path.display(), "Landlock allow read-write"); + ruleset = ruleset + .add_rule(PathBeneath::new(path_fd, allowed_access)) + .into_diagnostic()?; } let skipped = total_paths - rules_applied; @@ -469,11 +472,119 @@ fn access_for_path_fd( }) } +/// A path-opening failure that must not be confused with compatibility errors. +#[derive(Debug)] +enum PolicyPathError { + Io(std::io::Error), + UnsafeSymlink, +} + +impl From for PolicyPathError { + fn from(error: rustix::io::Errno) -> Self { + Self::Io(error.into()) + } +} + +/// Whether a directory's entries can be changed by this workload identity. +/// Ownership counts even without current write permission: owners can chmod. +/// An unrecognized access-check failure is conservatively treated as mutable. +fn workload_can_modify_directory(directory: &OwnedFd) -> Result { + use rustix::fs::{Access, AtFlags, accessat, fstat}; + let stat = fstat(directory)?; + if stat.st_uid == rustix::process::geteuid().as_raw() { + return Ok(true); + } + // A read-only image mount is also immutable to the capability-free + // workload. Unknown failures remain untrusted rather than granting access. + Ok(!matches!( + accessat(directory, ".", Access::WRITE_OK, AtFlags::EACCESS), + Err(rustix::io::Errno::ACCESS | rustix::io::Errno::ROFS) + )) +} + +fn path_components(path: &Path) -> VecDeque { + path.components() + .filter_map(|component| match component { + Component::RootDir | Component::CurDir => None, + Component::ParentDir => Some(OsString::from("..")), + Component::Normal(name) => Some(name.to_owned()), + Component::Prefix(_) => unreachable!("Linux paths have no prefix"), + }) + .collect() +} + +/// Resolve policy anchors through pinned descriptors, following only symlinks +/// beneath directories the workload cannot modify. This preserves immutable +/// image aliases while refusing mutable aliases at every path component. +fn open_policy_path(path: &Path) -> Result { + use rustix::fs::{FileType, Mode, OFlags, fstat, open, openat, readlinkat}; + if !path.is_absolute() { + return Err(PolicyPathError::Io(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Landlock policy paths must be absolute", + ))); + } + let flags = OFlags::PATH | OFlags::NOFOLLOW | OFlags::CLOEXEC; + let mut directory = open("/", flags | OFlags::DIRECTORY, Mode::empty())?; + let mut mutable_ancestor = workload_can_modify_directory(&directory)?; + let mut pending = path_components(path); + let mut symlinks = 0; + while let Some(component) = pending.pop_front() { + let fd = openat(&directory, &component, flags, Mode::empty())?; + match FileType::from_raw_mode(fstat(&fd)?.st_mode) { + FileType::Symlink => { + // A non-writable immediate parent is insufficient: an ancestor + // may allow renaming that parent or replacing its entire tree. + if mutable_ancestor { + return Err(PolicyPathError::UnsafeSymlink); + } + symlinks += 1; + if symlinks > 40 { + return Err(rustix::io::Errno::LOOP.into()); + } + // Empty-path readlinkat reads the opened link, not a possibly + // replaced directory entry. Magic links are never opened. + let target = PathBuf::from(OsString::from_vec( + readlinkat(&fd, "", Vec::new())?.into_bytes(), + )); + if target.is_absolute() { + directory = open("/", flags | OFlags::DIRECTORY, Mode::empty())?; + mutable_ancestor |= workload_can_modify_directory(&directory)?; + } + let mut target_components = path_components(&target); + target_components.append(&mut pending); + pending = target_components; + } + FileType::Directory => { + mutable_ancestor |= workload_can_modify_directory(&fd)?; + directory = fd; + } + _ if pending.is_empty() => return Ok(fd), + _ => return Err(rustix::io::Errno::NOTDIR.into()), + } + } + Ok(directory) +} + +fn open_policy_paths( + paths: &[PathBuf], + compatibility: &LandlockCompatibility, + path_open_mode: PathOpenMode, +) -> Result> { + let mut opened = Vec::new(); + for path in paths { + if let Some(fd) = try_open_path(path, compatibility, path_open_mode)? { + opened.push((path.clone(), fd)); + } + } + Ok(opened) +} + /// Attempt to open a path for Landlock rule creation. /// -/// In `BestEffort` mode, inaccessible paths (missing, permission denied, symlink -/// loops, etc.) are skipped with a warning and `Ok(None)` is returned so the -/// caller can continue building the ruleset from the remaining valid paths. +/// Mutable symlink traversal is always fatal. In `BestEffort` mode, ordinary +/// inaccessible paths are skipped with `Ok(None)` so the caller can continue +/// building the ruleset from the remaining valid paths. /// /// In `HardRequirement` mode, any failure is fatal — the caller propagates the /// error, which ultimately aborts sandbox startup. @@ -481,16 +592,16 @@ fn try_open_path( path: &Path, compatibility: &LandlockCompatibility, path_open_mode: PathOpenMode, -) -> Result> { - match PathFd::new(path) { +) -> Result> { + match open_policy_path(path) { Ok(fd) => Ok(Some(fd)), - Err(err) => { - let reason = classify_path_fd_error(&err); - let is_not_found = matches!( - &err, - PathFdError::OpenCall { source, .. } - if source.kind() == std::io::ErrorKind::NotFound - ); + Err(PolicyPathError::UnsafeSymlink) => Err(miette::miette!( + "Landlock policy path traverses a symlink beneath a workload-modifiable directory: {}", + path.display() + )), + Err(PolicyPathError::Io(err)) => { + let reason = classify_io_error(&err); + let is_not_found = err.kind() == std::io::ErrorKind::NotFound; if matches!(path_open_mode, PathOpenMode::CurrentUser) { if is_not_found { debug!( @@ -556,19 +667,6 @@ fn try_open_path( } } -/// Classify a [`PathFdError`] into a human-readable reason. -/// -/// `PathFd::new()` wraps `open(path, O_PATH | O_CLOEXEC)` which can fail for -/// several reasons beyond simple non-existence. The `PathFdError::OpenCall` -/// variant wraps the underlying `std::io::Error`. -fn classify_path_fd_error(err: &PathFdError) -> &'static str { - match err { - PathFdError::OpenCall { source, .. } => classify_io_error(source), - // PathFdError is #[non_exhaustive], handle future variants gracefully. - _ => "unexpected error", - } -} - /// Classify a `std::io::Error` into a human-readable reason string. fn classify_io_error(err: &std::io::Error) -> &'static str { match err.kind() { @@ -884,6 +982,171 @@ mod tests { assert!(result.unwrap().is_none()); } + #[test] + fn policy_path_rejects_mutable_symlink_in_every_compatibility_mode() { + let root = tempfile::tempdir().unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink("/", &link).unwrap(); + for compatibility in [ + LandlockCompatibility::BestEffort, + LandlockCompatibility::HardRequirement, + ] { + for mode in [PathOpenMode::CurrentUser, PathOpenMode::Privileged] { + let error = try_open_path(&link, &compatibility, mode).unwrap_err(); + assert!(error.to_string().contains("workload-modifiable")); + } + } + } + + #[test] + fn policy_path_rejects_mutable_ancestor_symlink() { + let root = tempfile::tempdir().unwrap(); + let outside = tempfile::tempdir().unwrap(); + std::fs::create_dir(outside.path().join("nested")).unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink(outside.path(), &link).unwrap(); + assert!(matches!( + open_policy_path(&link.join("nested")), + Err(PolicyPathError::UnsafeSymlink) + )); + } + + #[test] + fn policy_path_owned_readonly_directory_is_mutable() { + use std::os::unix::fs::PermissionsExt; + let root = tempfile::tempdir().unwrap(); + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o555)).unwrap(); + let fd = rustix::fs::open( + root.path(), + rustix::fs::OFlags::PATH | rustix::fs::OFlags::DIRECTORY, + rustix::fs::Mode::empty(), + ) + .unwrap(); + assert!(workload_can_modify_directory(&fd).unwrap()); + // Restore permissions for TempDir cleanup. + std::fs::set_permissions(root.path(), std::fs::Permissions::from_mode(0o700)).unwrap(); + } + + #[test] + fn policy_path_descriptor_stays_on_admitted_inode_after_replacement() { + let root = tempfile::tempdir().unwrap(); + let workspace = root.path().join("workspace"); + std::fs::create_dir(&workspace).unwrap(); + let fd = open_policy_path(&workspace).unwrap(); + let admitted = rustix::fs::fstat(&fd).unwrap(); + std::fs::remove_dir(&workspace).unwrap(); + std::os::unix::fs::symlink("/", &workspace).unwrap(); + let retained = rustix::fs::fstat(&fd).unwrap(); + assert_eq!( + (retained.st_dev, retained.st_ino), + (admitted.st_dev, admitted.st_ino) + ); + assert!(matches!( + open_policy_path(&workspace), + Err(PolicyPathError::UnsafeSymlink) + )); + } + + #[test] + fn policy_path_rejects_relative_paths() { + assert!(matches!( + open_policy_path(Path::new("workspace")), + Err(PolicyPathError::Io(_)) + )); + } + + #[test] + fn policy_path_trusted_image_aliases_and_mutable_ancestry() { + use std::os::unix::fs::PermissionsExt; + use std::os::unix::process::CommandExt; + // Exercise the actual DAC check as an unprivileged workload. Root owns + // every containing system directory and is not an admitted workload UID. + if rustix::process::geteuid().is_root() { + let fixture = tempfile::tempdir().unwrap(); + std::fs::set_permissions(fixture.path(), std::fs::Permissions::from_mode(0o777)) + .unwrap(); + let immutable = fixture.path().join("immutable"); + std::fs::create_dir(&immutable).unwrap(); + std::os::unix::fs::symlink("/", immutable.join("alias")).unwrap(); + std::fs::set_permissions(&immutable, std::fs::Permissions::from_mode(0o555)).unwrap(); + let mut command = std::process::Command::new(std::env::current_exe().unwrap()); + command.args([ + "--exact", + "sandbox::linux::landlock::tests::policy_path_trusted_image_aliases_and_mutable_ancestry", + "--nocapture", + ]).env("OPENSHELL_LANDLOCK_MUTABLE_ANCESTRY", immutable.join("alias")); + // SAFETY: these credential operations execute only in the forked + // test child before exec and do not change the test runner identity. + #[allow(unsafe_code)] + unsafe { + command.pre_exec(|| { + if libc::setgroups(0, std::ptr::null()) != 0 + || libc::setgid(65534) != 0 + || libc::setuid(65534) != 0 + { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + } + let status = command.status().unwrap(); + std::fs::set_permissions(&immutable, std::fs::Permissions::from_mode(0o755)).unwrap(); + assert!(status.success()); + return; + } + for alias in ["/bin", "/lib"] { + if std::fs::symlink_metadata(alias).is_ok() { + open_policy_path(Path::new(alias)).unwrap(); + } + } + if let Some(path) = std::env::var_os("OPENSHELL_LANDLOCK_MUTABLE_ANCESTRY") { + assert!(matches!( + open_policy_path(Path::new(&path)), + Err(PolicyPathError::UnsafeSymlink) + )); + } + } + + #[test] + fn prepare_mutable_policy_anchor_never_uses_best_effort_fallback() { + if !matches!(probe_availability(), LandlockAvailability::Available { .. }) { + return; + } + let root = tempfile::tempdir().unwrap(); + let link = root.path().join("workspace"); + std::os::unix::fs::symlink("/", &link).unwrap(); + for compatibility in [ + LandlockCompatibility::BestEffort, + LandlockCompatibility::HardRequirement, + ] { + for mode in [PathOpenMode::CurrentUser, PathOpenMode::Privileged] { + let mut policy = + hard_requirement_policy(vec![root.path().to_path_buf()], vec![link.clone()]); + policy.landlock.compatibility = compatibility.clone(); + let error = prepare_with_path_open_mode(&policy, None, mode) + .err() + .expect("unsafe anchor must abort preparation"); + assert!(error.to_string().contains("workload-modifiable")); + } + } + } + + #[test] + fn prepare_zero_usable_paths_never_uses_best_effort_fallback() { + if !matches!(probe_availability(), LandlockAvailability::Available { .. }) { + return; + } + let mut policy = hard_requirement_policy( + vec![], + vec![PathBuf::from("/nonexistent/openshell/test/path")], + ); + policy.landlock.compatibility = LandlockCompatibility::BestEffort; + let error = prepare_current_user(&policy, None) + .err() + .expect("empty allowlist must fail closed"); + assert!(error.to_string().contains("zero valid paths")); + } + #[test] fn classify_not_found() { let err = std::io::Error::from_raw_os_error(libc::ENOENT); @@ -920,14 +1183,6 @@ mod tests { assert_eq!(classify_io_error(&err), "unexpected error"); } - #[test] - fn classify_path_fd_error_extracts_io_error() { - // Use PathFd::new on a non-existent path to get a real PathFdError - // (the OpenCall variant is #[non_exhaustive] and can't be constructed directly). - let err = PathFd::new("/nonexistent/openshell/classify/test").unwrap_err(); - assert_eq!(classify_path_fd_error(&err), "path does not exist"); - } - #[test] fn probe_availability_returns_a_result() { // The probe should not panic regardless of whether Landlock is available. diff --git a/crates/openshell-supervisor-process/src/ssh.rs b/crates/openshell-supervisor-process/src/ssh.rs index 63ae30fbcc..795838da17 100644 --- a/crates/openshell-supervisor-process/src/ssh.rs +++ b/crates/openshell-supervisor-process/src/ssh.rs @@ -46,6 +46,7 @@ const MAIN_DETACH_KEY: u8 = 0x11; const MAIN_DETACH_EOF: u8 = 0x04; const SSH_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15); const SSH_PEER_TIMEOUT: Duration = Duration::from_mins(1); +const STARTUP_DIAGNOSTIC_TIMEOUT: Duration = Duration::from_millis(50); mod peer_stream; @@ -392,6 +393,7 @@ async fn handle_connection( #[allow(clippy::struct_excessive_bools)] #[derive(Default)] struct ChannelState { + output: Option>, upload: Option, command_started: bool, input_sender: Option, @@ -501,7 +503,15 @@ impl russh::server::Handler for SshHandler { reply: ChannelOpenHandle, _session: &mut Session, ) -> Result<(), Self::Error> { - self.channels.insert(channel.id(), ChannelState::default()); + let id = channel.id(); + let (_, output) = channel.split(); + self.channels.insert( + id, + ChannelState { + output: Some(output), + ..Default::default() + }, + ); reply.accept().await; Ok(()) } @@ -687,11 +697,9 @@ impl russh::server::Handler for SshHandler { channel: ChannelId, session: &mut Session, ) -> Result<(), Self::Error> { - if self - .channels - .get(&channel) - .is_some_and(|state| state.upload.is_some() || state.command_started) - { + if self.channels.get(&channel).is_some_and(|state| { + state.upload.is_some() || state.command_started || state.main_attached + }) { session.channel_failure(channel)?; return Ok(()); } @@ -701,11 +709,15 @@ impl russh::server::Handler for SshHandler { // endings. Forcing a PTY here caused CRLF translation which made // VS Code misdetect the platform as Windows (and then try to run // `powershell`). + let terminal = self + .channels + .get(&channel) + .is_some_and(|state| state.pty_request.is_some()); match self.start_shell(channel, session.handle(), None).await { Ok(()) => session.channel_success(channel)?, Err(error) => { warn!(%error, ?channel, "failed to start boundary shell"); - session.channel_failure(channel)?; + self.finish_failed_start(channel, session, &error, terminal)?; } } Ok(()) @@ -717,11 +729,9 @@ impl russh::server::Handler for SshHandler { data: &[u8], session: &mut Session, ) -> Result<(), Self::Error> { - if self - .channels - .get(&channel) - .is_some_and(|state| state.upload.is_some() || state.command_started) - { + if self.channels.get(&channel).is_some_and(|state| { + state.upload.is_some() || state.command_started || state.main_attached + }) { session.channel_failure(channel)?; return Ok(()); } @@ -730,6 +740,10 @@ impl russh::server::Handler for SshHandler { session.channel_success(channel)?; return Ok(()); } + let terminal = self + .channels + .get(&channel) + .is_some_and(|state| state.pty_request.is_some()); match self .start_shell(channel, session.handle(), Some(command)) .await @@ -737,7 +751,7 @@ impl russh::server::Handler for SshHandler { Ok(()) => session.channel_success(channel)?, Err(error) => { warn!(%error, ?channel, "failed to start boundary exec"); - session.channel_failure(channel)?; + self.finish_failed_start(channel, session, &error, terminal)?; } } Ok(()) @@ -1237,6 +1251,56 @@ impl SshHandler { Ok(()) } + /// Complete an accepted command request whose boundary process cannot start. + /// Request failure alone leaves clients waiting for command output or exit. + fn finish_failed_start( + &mut self, + channel: ChannelId, + session: &mut Session, + error: &anyhow::Error, + terminal: bool, + ) -> anyhow::Result<()> { + let mut output = None; + if let Some(mut state) = self.channels.remove(&channel) { + output = state.output.take(); + state.input_task.take(); + // A PTY resize can fail after spawning. Release that process too. + if let Some(process) = state.process.take() { + tokio::spawn(async move { + if let Err(error) = process.terminate().await { + warn!(%error, "failed to terminate exec after startup failure"); + } + }); + } + } + let line_ending = if terminal { "\r\n" } else { "\n" }; + session.channel_success(channel)?; + if let Some(output) = output { + let diagnostic = + format!("openshell: cannot start command: {error}{line_ending}").into_bytes(); + tokio::spawn(async move { + // Reserve client credit before queueing bytes. Session output + // would buffer stderr without credit and block closure and + // Handle messages for every channel on this connection. + let _ = tokio::time::timeout( + STARTUP_DIAGNOSTIC_TIMEOUT, + output.extended_data_bytes(1, diagnostic), + ) + .await; + // Diagnostics may be partial or absent; terminal messages + // must still be sent when the peer withholds output credit. + let _ = output.exit_status(1).await; + let _ = output.eof().await; + let _ = output.close().await; + }); + } else { + session.exit_status_request(channel, 1)?; + session.eof(channel)?; + session.close(channel)?; + } + Ok(()) + } + async fn start_shell( &mut self, channel: ChannelId, @@ -1969,6 +2033,125 @@ mod tests { )); } + async fn assert_boundary_start_failure(shell: bool, terminal: bool) { + let client = authenticated_test_client().await; + let mut channel = client.channel_open_session().await.unwrap(); + if terminal { + channel + .request_pty(true, "xterm", 80, 24, 0, 0, &[]) + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Success + )); + } + if shell { + channel.request_shell(true).await.unwrap(); + } else { + channel.exec(true, "true").await.unwrap(); + } + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Success + )); + match next_main_event(&mut channel).await { + russh::ChannelMsg::ExtendedData { data, ext: 1 } => { + let message = String::from_utf8_lossy(&data); + assert!( + message.starts_with("openshell: cannot start command: "), + "{message}" + ); + assert!( + message.contains("exec is not used by direct-tcpip tests"), + "{message}" + ); + assert!(message.ends_with(if terminal { "\r\n" } else { "\n" })); + } + event => panic!("expected startup diagnostic on stderr, got {event:?}"), + } + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::ExitStatus { exit_status: 1 } + )); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Eof + )); + assert!(matches!( + next_main_event(&mut channel).await, + russh::ChannelMsg::Close + )); + assert!( + tokio::time::timeout(Duration::from_secs(5), channel.wait()) + .await + .expect("failed command channel must close") + .is_none() + ); + } + + #[tokio::test] + async fn boundary_exec_start_failure_reports_error_and_closes_channel() { + for terminal in [false, true] { + assert_boundary_start_failure(false, terminal).await; + } + } + + #[tokio::test] + async fn boundary_shell_start_failure_reports_error_and_closes_channel() { + for terminal in [false, true] { + assert_boundary_start_failure(true, terminal).await; + } + } + + #[tokio::test] + async fn main_attachment_rejects_extra_commands_without_losing_input_lease() { + for shell in [false, true] { + tokio::time::timeout(Duration::from_secs(5), async { + let (main, mut input) = MainSession::inert_with_input(); + let client = main_test_client(Some(main.clone())).await; + let mut attached = client.channel_open_session().await.unwrap(); + attached + .request_subsystem(true, "openshell-main") + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut attached).await, + russh::ChannelMsg::Success + )); + if shell { + attached.request_shell(true).await.unwrap(); + } else { + attached.exec(true, "extra").await.unwrap(); + } + assert!(matches!( + next_main_event(&mut attached).await, + russh::ChannelMsg::Failure + )); + attached.data(&b"still attached"[..]).await.unwrap(); + assert_eq!(input.recv().await.unwrap(), b"still attached"); + attached.close().await.unwrap(); + // Processing a new channel follows the previous close on the + // wire, so the old owner's lease must already be released. + let mut fresh = client.channel_open_session().await.unwrap(); + fresh + .request_subsystem(true, "openshell-main") + .await + .unwrap(); + assert!(matches!( + next_main_event(&mut fresh).await, + russh::ChannelMsg::Success + )); + fresh.data(&b"new owner"[..]).await.unwrap(); + assert_eq!(input.recv().await.unwrap(), b"new owner"); + assert!(!main.finished()); + fresh.close().await.unwrap(); + }) + .await + .expect("extra command stranded main attachment or input lease"); + } + } + #[tokio::test] async fn main_attachment_missing_session_reports_terminal_error() { assert_main_rejection(None, false, "openshell: cannot connect to the canonical main process: no main session is available\n").await; diff --git a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs index 35c16cf75d..bc3621b93f 100644 --- a/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs +++ b/crates/openshell-supervisor-process/src/ssh/exec_input_tests.rs @@ -197,7 +197,19 @@ async fn exec_start_failure_keeps_ssh_session_usable() { let mut rejected = client.channel_open_session().await.unwrap(); rejected.exec(true, "first").await.unwrap(); - assert!(matches!(rejected.wait().await, Some(ChannelMsg::Failure))); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Success))); + match rejected.wait().await { + Some(ChannelMsg::ExtendedData { data, ext: 1 }) => { + assert!(String::from_utf8_lossy(&data).contains("test boundary admission failure")); + } + event => panic!("expected boundary failure diagnostic, got {event:?}"), + } + assert!(matches!( + rejected.wait().await, + Some(ChannelMsg::ExitStatus { exit_status: 1 }) + )); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Eof))); + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Close))); let mut channel = start_exec(&client).await; control.stdout.shutdown().await.unwrap(); @@ -218,6 +230,87 @@ async fn exec_start_failure_keeps_ssh_session_usable() { .expect("a rejected exec closed the SSH session"); } +#[tokio::test] +async fn startup_failure_backpressure_does_not_block_sibling_completion() { + for window_size in [0, 8, 65_536] { + for shell in [false, true] { + tokio::time::timeout(DEADLINE, async { + let (session, mut control) = exec_session(); + let client = test_client( + None, + rpc_test::exec(Arc::new(FailThenExec { + failed: AtomicBool::new(false), + session: Mutex::new(Some(session)), + })), + russh::client::Config { + window_size, + ..Default::default() + }, + ) + .await; + let mut rejected = client.channel_open_session().await.unwrap(); + if shell { + rejected.request_shell(true).await.unwrap(); + } else { + rejected.exec(true, "rejected").await.unwrap(); + } + assert!(matches!(rejected.wait().await, Some(ChannelMsg::Success))); + let mut sibling = start_exec(&client).await; + control.stdout.shutdown().await.unwrap(); + control.stderr.shutdown().await.unwrap(); + control + .process + .status + .send_replace(Some(BoundaryExitStatus::Exited(0))); + // Withhold reads on the failed channel until its sibling has + // finished: a small receive window must not stall the session. + let mut sibling_exited = false; + while let Some(event) = sibling.wait().await { + match event { + ChannelMsg::ExitStatus { exit_status: 0 } => sibling_exited = true, + ChannelMsg::Close => break, + _ => {} + } + } + assert!(sibling_exited, "sibling must report its exit status"); + let mut exited = false; + let mut eof = false; + let mut closed = false; + let mut diagnostic = Vec::new(); + while let Some(event) = rejected.wait().await { + match event { + ChannelMsg::ExtendedData { data, ext: 1 } => { + diagnostic.extend_from_slice(&data); + } + ChannelMsg::ExitStatus { exit_status: 1 } => exited = true, + ChannelMsg::Eof => eof = true, + ChannelMsg::Close => { + closed = true; + break; + } + event => panic!("unexpected failure event: {event:?}"), + } + } + assert!( + exited && eof && closed, + "failure must finish independently of stderr" + ); + if window_size == 0 { + assert!(diagnostic.is_empty()); + } + if window_size == 65_536 { + assert!( + String::from_utf8_lossy(&diagnostic) + .contains("test boundary admission failure") + ); + } + }) + .await + .expect("startup diagnostic blocked channel closure or sibling completion"); + } + } +} + #[tokio::test] async fn full_input_allows_output_signals_and_channel_close() { tokio::time::timeout(DEADLINE, async { diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 40faf25379..15a2bc7b0c 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -55,6 +55,11 @@ These defaults and the filesystem path restrictions below also apply when the su Each path must be absolute, must not contain `..`, and must not exceed 4096 bytes. `read_write` cannot contain `/`. A policy can list at most 256 paths. +Policy paths can follow system symlinks only when the workload cannot modify +any containing directory. A symlink beneath a directory the workload owns or +can write causes launch to fail, including in `best_effort` mode. List the +target directory directly for links inside a writable workspace. + ```yaml showLineNumbers={false} filesystem_policy: include_workdir: true @@ -69,8 +74,8 @@ filesystem_policy: | `compatibility` | string | `best_effort` | `best_effort` or `hard_requirement` | Both values skip individual paths that are missing or that the workload cannot -open. They differ when none of the listed paths can be applied, or when Landlock -fails to enforce the policy's rules: +open. If none of the listed paths can be applied, launch fails in either mode. +They differ when Landlock fails to build or enforce the policy's rules: | Value | Behavior | |---|---| diff --git a/e2e/rust/tests/landlock.rs b/e2e/rust/tests/landlock.rs index ae1e16601f..cfbdcd5846 100644 --- a/e2e/rust/tests/landlock.rs +++ b/e2e/rust/tests/landlock.rs @@ -6,6 +6,7 @@ //! End-to-end coverage for Landlock filesystem enforcement. use std::io::Write; +use std::time::Duration; use openshell_e2e::harness::sandbox::SandboxGuard; use tempfile::NamedTempFile; @@ -13,6 +14,10 @@ use tempfile::NamedTempFile; const SUCCESS_MARKER: &str = "landlock-hard-requirement-ok"; fn write_hard_requirement_policy() -> Result { + write_policy("hard_requirement", "/sandbox") +} + +fn write_policy(compatibility: &str, extra_path: &str) -> Result { let mut file = NamedTempFile::new().map_err(|error| format!("create temporary policy: {error}"))?; let policy = r#"version: 1 @@ -45,6 +50,15 @@ network_policies: - path: "/**" "#; + let policy = policy + .replace( + "compatibility: hard_requirement", + &format!("compatibility: {compatibility}"), + ) + .replace( + " - /sandbox\n", + &format!(" - /sandbox\n - {extra_path}\n"), + ); file.write_all(policy.as_bytes()) .map_err(|error| format!("write temporary policy: {error}"))?; file.flush() @@ -72,3 +86,65 @@ async fn hard_requirement_accepts_enriched_device_path() { assert!(sandbox.create_output.contains(SUCCESS_MARKER)); sandbox.cleanup().await; } + +async fn check_policy_path_replacement(compatibility: &str, replace_ancestor: bool) { + let policy = write_policy(compatibility, "/sandbox/policy-parent/leaf") + .expect("write filesystem policy"); + let policy_path = policy.path().to_string_lossy().into_owned(); + let mut sandbox = SandboxGuard::create(&["--policy", &policy_path]) + .await + .expect("create sandbox with filesystem policy"); + + sandbox + .exec(&["sh", "-c", "mkdir -p /sandbox/policy-parent/leaf"]) + .await + .expect("create declared directory under writable workspace"); + sandbox + .exec(&[ + "sh", + "-c", + "echo allowed > /sandbox/policy-parent/leaf/check", + ]) + .await + .expect("normal directory should remain usable on the next exec"); + + let script = if replace_ancestor { + "mv /sandbox/policy-parent /sandbox/original-parent; ln -s /var/tmp /sandbox/policy-parent" + } else { + "mv /sandbox/policy-parent/leaf /sandbox/original-leaf; ln -s /var/tmp /sandbox/policy-parent/leaf" + }; + sandbox + .exec(&["sh", "-c", script]) + .await + .expect("replace workspace directory"); + let result = tokio::time::timeout(Duration::from_secs(20), sandbox.exec(&["true"])).await; + // Delete through the control plane even when further workload launches fail. + sandbox.cleanup().await; + let error = result + .expect("rejected launch must close its SSH channel promptly") + .expect_err("mutable policy symlink must prevent the next launch"); + assert!( + error.to_lowercase().contains("symlink"), + "unexpected launch error: {error}" + ); +} + +#[tokio::test] +async fn best_effort_rejects_replaced_policy_path() { + check_policy_path_replacement("best_effort", false).await; +} + +#[tokio::test] +async fn hard_requirement_rejects_replaced_policy_path() { + check_policy_path_replacement("hard_requirement", false).await; +} + +#[tokio::test] +async fn best_effort_rejects_replaced_policy_ancestor() { + check_policy_path_replacement("best_effort", true).await; +} + +#[tokio::test] +async fn hard_requirement_rejects_replaced_policy_ancestor() { + check_policy_path_replacement("hard_requirement", true).await; +} diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 80690c45d1..9dcb5d9d8d 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -519,6 +519,10 @@ network_policies: ``` The `filesystem_policy` and `landlock` sections above are sensible defaults. +Use actual target paths for symlinks inside writable workspaces. Policy paths +that traverse symlinks beneath workload-owned or writable directories fail +launch even in `best_effort` mode; system aliases through protected directories +remain supported. A policy with no usable filesystem paths also fails launch. Process identity is omitted so the selected compute driver can choose it. For Docker and Podman, each omitted identity field falls back to the image's OCI `USER`. Tell the user these are defaults and may need adjustment for their From 9321d1632a41cd7337db38ff982164bff9120d08 Mon Sep 17 00:00:00 2001 From: "John T. Myers" <9696606+johntmyers@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:32:44 +0000 Subject: [PATCH 21/31] fix(network): align REST and WebSocket endpoint handling (#5336) Signed-off-by: John Myers <9696606+johntmyers@users.noreply.github.com> --- crates/openshell-policy-schema/src/lib.rs | 2 +- crates/openshell-policy/src/l7_validate.rs | 45 +++++ crates/openshell-policy/src/lib.rs | 17 +- crates/openshell-policy/src/merge.rs | 5 +- crates/openshell-providers/src/profiles.rs | 37 ++++ .../src/l7/mod.rs | 27 ++- .../src/l7/relay.rs | 180 ++++++++++++++++-- .../src/l7/rest.rs | 30 +-- .../openshell-supervisor-network/src/opa.rs | 6 +- docs/how-it-works/policies/network-rules.mdx | 5 + docs/how-it-works/policies/schema.mdx | 4 +- docs/how-it-works/providers/overview.mdx | 3 +- e2e/rust/tests/forward_proxy_l7_bypass.rs | 59 ++++++ skills/generate-sandbox-policy/SKILL.md | 1 + 14 files changed, 374 insertions(+), 47 deletions(-) diff --git a/crates/openshell-policy-schema/src/lib.rs b/crates/openshell-policy-schema/src/lib.rs index 656433b794..422ac13440 100644 --- a/crates/openshell-policy-schema/src/lib.rs +++ b/crates/openshell-policy-schema/src/lib.rs @@ -285,7 +285,7 @@ pub struct NetworkEndpoint { /// Defaults to false (strict). #[serde(default, skip_serializing_if = "std::ops::Not::not")] pub allow_encoded_slash: bool, - /// When true, client-to-server WebSocket text messages on this REST + /// When true, client-to-server WebSocket text messages on a `protocol: websocket` /// endpoint rewrite credential placeholders after an allowed 101 upgrade. /// Defaults to false. #[serde(default, skip_serializing_if = "std::ops::Not::not")] diff --git a/crates/openshell-policy/src/l7_validate.rs b/crates/openshell-policy/src/l7_validate.rs index ac995fa65b..74be95cc82 100644 --- a/crates/openshell-policy/src/l7_validate.rs +++ b/crates/openshell-policy/src/l7_validate.rs @@ -223,6 +223,9 @@ pub struct L7EndpointFields<'a> { /// `true` when the endpoint has a non-empty rules list. pub has_rules: bool, + /// Whether WebSocket credential rewriting was enabled. + pub websocket_credential_rewrite: bool, + /// `true` when the endpoint has a non-empty `deny_rules` list. pub has_deny_rules: bool, @@ -244,6 +247,9 @@ pub fn validate_l7_endpoint_semantics(ep: &L7EndpointFields<'_>) -> Vec let mut errors = Vec::new(); let protocol = ep.protocol; let l7_protocol = L7Protocol::parse(protocol); + if matches!(l7_protocol, Some(L7Protocol::Rest)) && ep.websocket_credential_rewrite { + errors.push("websocket_credential_rewrite is not supported for protocol rest; use protocol: websocket with explicit message rules".to_string()); + } let explicit_tcp = is_explicit_tcp_protocol(protocol); let jsonrpc_family = l7_protocol.is_some_and(L7Protocol::is_jsonrpc_family); let is_mcp = matches!(l7_protocol, Some(L7Protocol::Mcp)); @@ -335,6 +341,7 @@ mod tests { fn valid_rest_endpoint() -> L7EndpointFields<'static> { L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "read-only", has_rules: false, @@ -344,6 +351,27 @@ mod tests { } } + #[test] + fn rest_rejects_websocket_credential_rewrite_with_migration_hint() { + let mut endpoint = valid_rest_endpoint(); + endpoint.websocket_credential_rewrite = true; + for protocol in ["rest", "REST", "Rest"] { + endpoint.protocol = protocol; + let errors = validate_l7_endpoint_semantics(&endpoint); + assert!( + errors.iter().any(|e| e.contains("use protocol: websocket")), + "{protocol}: {errors:?}" + ); + } + for protocol in ["websocket", "WEBSOCKET", "WebSocket"] { + endpoint.protocol = protocol; + assert!( + validate_l7_endpoint_semantics(&endpoint).is_empty(), + "{protocol}" + ); + } + } + #[test] fn valid_endpoint_produces_no_errors() { let errors = validate_l7_endpoint_semantics(&valid_rest_endpoint()); @@ -383,6 +411,7 @@ mod tests { #[test] fn rejects_unknown_protocol() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "ftp", access: "", has_rules: false, @@ -397,6 +426,7 @@ mod tests { #[test] fn rejects_rules_and_access_together() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "full", has_rules: true, @@ -411,6 +441,7 @@ mod tests { #[test] fn rejects_jsonrpc_with_access_presets() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "full", has_rules: false, @@ -429,6 +460,7 @@ mod tests { #[test] fn rejects_mcp_with_access_presets() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "full", has_rules: false, @@ -447,6 +479,7 @@ mod tests { #[test] fn rejects_jsonrpc_without_rules() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "", has_rules: false, @@ -461,6 +494,7 @@ mod tests { #[test] fn rejects_protocol_without_rules_or_access() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: false, @@ -479,6 +513,7 @@ mod tests { #[test] fn rejects_mcp_without_rules_when_allow_all_false() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: false, @@ -493,6 +528,7 @@ mod tests { #[test] fn accepts_mcp_with_allow_all_true() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: false, @@ -507,6 +543,7 @@ mod tests { #[test] fn rejects_rules_that_deny_all() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: true, @@ -521,6 +558,7 @@ mod tests { #[test] fn mcp_allow_all_with_empty_allow_not_rejected() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "mcp", access: "", has_rules: true, @@ -538,6 +576,7 @@ mod tests { #[test] fn rejects_deny_rules_without_protocol() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "", access: "", has_rules: false, @@ -556,6 +595,7 @@ mod tests { #[test] fn rejects_deny_rules_without_allow_base() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "rest", access: "", has_rules: false, @@ -574,6 +614,7 @@ mod tests { #[test] fn no_protocol_no_errors() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "", access: "", has_rules: false, @@ -588,6 +629,7 @@ mod tests { #[test] fn explicit_tcp_is_valid_without_l7_fields() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "tcp", access: "", has_rules: false, @@ -604,6 +646,7 @@ mod tests { #[test] fn explicit_tcp_rejects_l7_fields() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "tcp", access: "full", has_rules: false, @@ -648,6 +691,7 @@ mod tests { #[test] fn jsonrpc_with_access_emits_single_diagnostic() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "full", has_rules: false, @@ -669,6 +713,7 @@ mod tests { #[test] fn jsonrpc_without_rules_or_access_emits_single_diagnostic() { let ep = L7EndpointFields { + websocket_credential_rewrite: false, protocol: "json-rpc", access: "", has_rules: false, diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index bad8532cdf..a84bca7868 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -1562,6 +1562,7 @@ fn validate_sandbox_policy_with_mcp_presence( }) }); let fields = L7EndpointFields { + websocket_credential_rewrite: ep.websocket_credential_rewrite, protocol: &ep.protocol, access, has_rules: !ep.rules.is_empty(), @@ -4786,6 +4787,18 @@ network_policies: ); } + #[test] + fn validate_rest_rejects_websocket_credential_rewrite() { + for protocol in ["rest", "REST", "Rest"] { + let yaml = "version: 1\nnetwork_policies:\n api:\n endpoints:\n - { host: api.example.com, port: 443, protocol: rest, access: read-only, websocket_credential_rewrite: true }\n"; + let yaml = yaml.replace("protocol: rest", &format!("protocol: {protocol}")); + let policy = parse_sandbox_policy(&yaml).unwrap(); + let errors = validate_sandbox_policy(&policy) + .expect_err("REST cannot rewrite WebSocket messages"); + assert!(format!("{errors:?}").contains("use protocol: websocket")); + } + } + #[test] fn round_trip_preserves_websocket_credential_rewrite() { let yaml = r" @@ -4796,7 +4809,7 @@ network_policies: endpoints: - host: gateway.example.com port: 443 - protocol: rest + protocol: websocket enforcement: enforce access: full websocket_credential_rewrite: true @@ -4808,7 +4821,7 @@ network_policies: let proto2 = parse_sandbox_policy(&yaml_out).expect("re-parse failed"); let ep = &proto2.network_policies["discord_gateway"].endpoints[0]; - assert_eq!(ep.protocol, "rest"); + assert_eq!(ep.protocol, "websocket"); assert!(ep.websocket_credential_rewrite); assert!(yaml_out.contains("websocket_credential_rewrite: true")); } diff --git a/crates/openshell-policy/src/merge.rs b/crates/openshell-policy/src/merge.rs index 4639af6123..c010e6a723 100644 --- a/crates/openshell-policy/src/merge.rs +++ b/crates/openshell-policy/src/merge.rs @@ -3818,8 +3818,8 @@ mod tests { /// agent is told its approved rule never landed. #[test] fn merged_policy_covers_the_rule_it_merged() { - // Every field `merge_endpoint` widens rather than replaces, set on the - // existing endpoint and absent from the proposal. + // REST-compatible fields that `merge_endpoint` widens rather than + // replaces, set on the existing endpoint and absent from the proposal. let existing_endpoint = NetworkEndpoint { protocol: "rest".to_string(), rules: vec![rest_rule("GET", "/health")], @@ -3830,7 +3830,6 @@ mod tests { }], allowed_ips: vec!["10.0.0.1".to_string()], allow_encoded_slash: true, - websocket_credential_rewrite: true, request_body_credential_rewrite: true, advisor_proposed: true, ..endpoint("api.example.com", 443) diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 3adfc9c278..b56dc2c7e4 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -2759,6 +2759,7 @@ pub fn validate_profile_set( } let l7_fields = L7EndpointFields { + websocket_credential_rewrite: endpoint.websocket_credential_rewrite, protocol: &endpoint.protocol, access: &endpoint.access, has_rules: endpoint.rules.as_ref().is_some_and(|r| !r.is_empty()), @@ -6330,6 +6331,42 @@ binaries: ["", /usr/bin/broken] assert!(messages.contains(&"binary path must not be empty")); } + #[test] + fn validate_profile_rejects_rest_websocket_credential_rewrite() { + for protocol in ["rest", "REST", "Rest"] { + let profile = parse_profile_yaml( + r" +id: rest-rewrite +display_name: REST rewrite +credentials: + - name: api_key + env_vars: [API_KEY] + auth_style: bearer + header_name: authorization +discovery: + credentials: [api_key] +endpoints: + - host: api.example.com + port: 443 + protocol: rest + access: read-only + websocket_credential_rewrite: true +binaries: + - /usr/bin/curl +" + .replace("protocol: rest", &format!("protocol: {protocol}")) + .as_str(), + ) + .unwrap(); + let diagnostics = validate_profile_set(&[("profile.yaml".to_string(), profile)]); + assert!( + diagnostics + .iter() + .any(|d| d.message.contains("use protocol: websocket")) + ); + } + } + #[test] fn validate_profile_set_rejects_noncanonical_profile_ids() { let profiles = [ diff --git a/crates/openshell-supervisor-network/src/l7/mod.rs b/crates/openshell-supervisor-network/src/l7/mod.rs index bc01d0bbdd..c24777826e 100644 --- a/crates/openshell-supervisor-network/src/l7/mod.rs +++ b/crates/openshell-supervisor-network/src/l7/mod.rs @@ -379,6 +379,9 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { let allow_encoded_slash = get_object_bool(val, "allow_encoded_slash").unwrap_or(false); let websocket_credential_rewrite = get_object_bool(val, "websocket_credential_rewrite").unwrap_or(false); + if protocol == L7Protocol::Rest && websocket_credential_rewrite { + return None; + } let request_body_credential_rewrite = get_object_bool(val, "request_body_credential_rewrite").unwrap_or(false); let allow_uninspected_credentials = @@ -1393,6 +1396,10 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec, Vec< .and_then(serde_json::Value::as_bool) .unwrap_or(false); let l7_fields = L7EndpointFields { + websocket_credential_rewrite: ep + .get("websocket_credential_rewrite") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), protocol, access, has_rules, @@ -1504,7 +1511,7 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec, Vec< && protocol != "websocket" { warnings.push(format!( - "{loc}: websocket_credential_rewrite is ignored unless protocol is rest or websocket" + "{loc}: websocket_credential_rewrite is ignored unless protocol is websocket" )); } @@ -2470,10 +2477,24 @@ mod tests { assert!(!config.websocket_credential_rewrite); } + #[test] + fn rest_websocket_credential_rewrite_fails_validation_and_materialization() { + let endpoint = serde_json::json!({ + "protocol": "rest", "host": "gateway.example.com", "port": 443, + "access": "read-only", "websocket_credential_rewrite": true + }); + let val = regorus::Value::from_json_str(&endpoint.to_string()).unwrap(); + assert!(parse_l7_config(&val).is_none()); + let (errors, _) = validate_l7_policies(&serde_json::json!({ + "network_policies": {"test": {"endpoints": [endpoint], "binaries": []}} + })); + assert!(errors.iter().any(|e| e.contains("use protocol: websocket"))); + } + #[test] fn parse_l7_config_websocket_credential_rewrite_opt_in() { let val = regorus::Value::from_json_str( - r#"{"protocol": "rest", "host": "gateway.example.com", "port": 443, "websocket_credential_rewrite": true}"#, + r#"{"protocol": "websocket", "host": "gateway.example.com", "port": 443, "websocket_credential_rewrite": true}"#, ) .unwrap(); let config = parse_l7_config(&val).unwrap(); @@ -2521,7 +2542,7 @@ mod tests { } #[test] - fn validate_websocket_credential_rewrite_warns_unless_rest_or_websocket() { + fn validate_websocket_credential_rewrite_warns_without_inspected_protocol() { let data = serde_json::json!({ "network_policies": { "test": { diff --git a/crates/openshell-supervisor-network/src/l7/relay.rs b/crates/openshell-supervisor-network/src/l7/relay.rs index fa20252714..61c342bc17 100644 --- a/crates/openshell-supervisor-network/src/l7/relay.rs +++ b/crates/openshell-supervisor-network/src/l7/relay.rs @@ -1990,8 +1990,7 @@ pub(crate) fn upgrade_options<'a>( engine: Option<&'a TunnelPolicyEngine>, ) -> UpgradeRelayOptions<'a> { let websocket_credential_rewrite = - matches!(config.protocol, L7Protocol::Rest | L7Protocol::Websocket) - && config.websocket_credential_rewrite; + config.protocol == L7Protocol::Websocket && config.websocket_credential_rewrite; let deny_uninspected_credentials = config.provider_credentialed && !config.allow_uninspected_credentials; let websocket_message_policy = if config.protocol == L7Protocol::Websocket { @@ -2035,7 +2034,6 @@ pub(crate) fn websocket_extension_mode( ) -> WebSocketExtensionMode { if inspecting_middleware_session || config.protocol == L7Protocol::Websocket - || (config.protocol == L7Protocol::Rest && config.websocket_credential_rewrite) || (config.provider_credentialed && !config.allow_uninspected_credentials) { WebSocketExtensionMode::PermessageDeflate @@ -2363,6 +2361,12 @@ where websocket_permessage_deflate, websocket_subprotocol, } => { + if crate::l7::rest::upgrade_refusal_for_protocol(config.protocol).is_some() { + if let Some(session) = middleware_session.take() { + session.end(openshell_core::proto::MiddlewareSessionEndReason::ProtocolError).await; + } + return Ok(()); + } let mut options = upgrade_options( config, ctx, @@ -5463,13 +5467,16 @@ network_policies: endpoints: - host: api.example.test port: 8080 - protocol: rest + protocol: websocket token_grant_owner: test-owner enforcement: enforce rules: - allow: method: GET path: "/v1/**" + - allow: + method: WEBSOCKET_TEXT + path: "/v1/**" binaries: - { path: /usr/bin/curl } "#; @@ -5525,7 +5532,10 @@ network_policies: ) .await .expect("send upgrade request"); - seen_rx.recv().await.expect("preflight reached middleware"); + tokio::time::timeout(std::time::Duration::from_secs(2), seen_rx.recv()) + .await + .expect("preflight timed out") + .expect("preflight reached middleware"); fixture.assert_no_requests(); release.notify_one(); @@ -10068,7 +10078,7 @@ network_policies: } #[tokio::test] - async fn rest_websocket_middleware_inspects_compressed_wss_messages() { + async fn websocket_middleware_inspects_compressed_wss_messages() { let data = r#" network_middlewares: redact: @@ -10082,12 +10092,15 @@ network_policies: endpoints: - host: api.example.test port: 8080 - protocol: rest + protocol: websocket enforcement: enforce rules: - allow: method: GET path: "/ws" + - allow: + method: WEBSOCKET_TEXT + path: "/ws" binaries: - { path: /usr/bin/node } "#; @@ -10176,7 +10189,7 @@ network_policies: endpoints: - host: gateway.example.test port: 443 - protocol: rest + protocol: websocket enforcement: enforce rules: - allow: @@ -10190,7 +10203,7 @@ network_policies: .clone_engine_for_tunnel(engine.current_generation()) .unwrap(); let configs = vec![L7EndpointConfig { - protocol: L7Protocol::Rest, + protocol: L7Protocol::Websocket, endpoint_id: String::new(), policy_hash: String::new(), path: "/ws".into(), @@ -10995,9 +11008,140 @@ network_policies: } #[tokio::test] - async fn route_selected_rest_websocket_upgrade_still_relays_beside_mcp() { - // The refusal follows the selected endpoint's protocol: a REST - // upgrade on the same host and port keeps its documented raw relay. + async fn rest_upgrade_refusal_emits_ocsf_denial() { + let (configs, _, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap(); + let request = crate::l7::provider::L7Request { + action: "GET".into(), + target: "/api/ws".into(), + query_params: std::collections::HashMap::new(), + raw_header: b"GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n".to_vec(), + body_length: crate::l7::provider::BodyLength::None, + }; + let capture = openshell_ocsf::test_support::OcsfCapture::default(); + let _guard = capture.install(); + let (_app, mut client) = tokio::io::duplex(8192); + assert!( + deny_unsupported_upgrade_if_requested(&request, config, &ctx, None, &mut client) + .await + .unwrap() + ); + let events = capture.json_events(); + assert!( + events.iter().any(|event| event["action_id"] == 2 + && event["status_detail"] == crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL), + "{events:?}" + ); + } + + #[tokio::test] + async fn rest_upgrades_are_refused_in_both_modes_and_relay_paths() { + for enforcement in [EnforcementMode::Enforce, EnforcementMode::Audit] { + for route_selected in [false, true] { + for upgrade in ["websocket", "custom"] { + let (mut configs, engine, ctx) = + jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter_mut() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap(); + config.enforcement = enforcement; + let config = config.clone(); + let request = format!( + "GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nConnection: keep-alive, Upgrade\r\nUpgrade: {upgrade}\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n" + ); + let frame = br#"{"action":"delete"}"#; + let scenario = run_upgrade_scenario( + request.as_bytes(), + frame, + move |mut client, mut upstream| { + tokio::spawn(async move { + if route_selected { + relay_with_route_selection( + &configs, + engine, + &mut client, + &mut upstream, + &ctx, + ) + .await + } else { + relay_with_inspection( + &config, + engine, + &mut client, + &mut upstream, + &ctx, + ) + .await + } + }) + }, + ) + .await; + assert_upgrade_refused_before_forwarding( + &scenario, + frame, + crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL, + ); + } + } + } + } + + #[tokio::test] + async fn rest_upgrade_after_ordinary_request_is_refused() { + for route_selected in [false, true] { + let (configs, engine, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); + let config = configs + .iter() + .find(|c| c.protocol == L7Protocol::Rest) + .unwrap() + .clone(); + let (mut app, mut client) = tokio::io::duplex(8192); + let (mut upstream, mut server) = tokio::io::duplex(8192); + let relay = tokio::spawn(async move { + if route_selected { + relay_with_route_selection(&configs, engine, &mut client, &mut upstream, &ctx) + .await + } else { + relay_with_inspection(&config, engine, &mut client, &mut upstream, &ctx).await + } + }); + app.write_all(b"GET /api/tickets HTTP/1.1\r\nHost: mcp.example.test:8000\r\n\r\n") + .await + .unwrap(); + let forwarded = read_http_headers(&mut server).await; + assert!(String::from_utf8_lossy(&forwarded).starts_with("GET /api/tickets ")); + server + .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + .await + .unwrap(); + let response = read_http_headers(&mut app).await; + assert!(String::from_utf8_lossy(&response).starts_with("HTTP/1.1 200")); + app.write_all(b"GET /api/ws HTTP/1.1\r\nHost: mcp.example.test:8000\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n").await.unwrap(); + let mut denied = Vec::new(); + tokio::time::timeout( + std::time::Duration::from_secs(2), + app.read_to_end(&mut denied), + ) + .await + .unwrap() + .unwrap(); + assert!(String::from_utf8_lossy(&denied).starts_with("HTTP/1.1 403")); + relay.await.unwrap().unwrap(); + let mut extra = Vec::new(); + server.read_to_end(&mut extra).await.unwrap(); + assert!(extra.is_empty(), "upgrade must not reach the upstream"); + } + } + + #[tokio::test] + async fn route_selected_rest_websocket_upgrade_is_refused_beside_mcp() { + // Both per-request protocols refuse upgrades on their selected path. let (configs, tunnel_engine, ctx) = jsonrpc_and_rest_route_configs("mcp", "enforce"); let frame = br#"{"type":"ping"}"#; let scenario = run_upgrade_scenario( @@ -11017,15 +11161,11 @@ network_policies: }, ) .await; - assert!( - scenario.response.starts_with("HTTP/1.1 101"), - "REST upgrade should still switch protocols, got: {}", - scenario.response + assert_upgrade_refused_before_forwarding( + &scenario, + frame, + crate::l7::rest::UNSUPPORTED_REST_UPGRADE_DETAIL, ); - assert!(contains_bytes( - &scenario.upstream_seen, - &masked_text_frame(frame) - )); } #[tokio::test] diff --git a/crates/openshell-supervisor-network/src/l7/rest.rs b/crates/openshell-supervisor-network/src/l7/rest.rs index 97c71f09b3..27f10ae7bd 100644 --- a/crates/openshell-supervisor-network/src/l7/rest.rs +++ b/crates/openshell-supervisor-network/src/l7/rest.rs @@ -88,6 +88,7 @@ pub(crate) const UNSUPPORTED_H2C_UPGRADE_DETAIL: &str = pub(crate) const UNSUPPORTED_JSONRPC_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for JSON-RPC or MCP endpoints"; pub(crate) const UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for GraphQL endpoints; serve GraphQL over WebSocket from a separate protocol: websocket endpoint on another path or port"; +pub(crate) const UNSUPPORTED_REST_UPGRADE_DETAIL: &str = "HTTP upgrade is not supported for REST endpoints; use a separate protocol: websocket endpoint with explicit message rules"; const MIN_HTTP2_PREFACE_DETECTION_BYTES: usize = 8; /// Idle timeout for `relay_until_eof`. If no data arrives within this window @@ -2442,11 +2443,11 @@ pub(crate) fn unsupported_upgrade_detail( /// individual HTTP requests, or `None` for a protocol that may relay an /// allowed upgrade. /// -/// JSON-RPC, MCP and GraphQL rules inspect each HTTP request body or query. +/// REST, JSON-RPC, MCP and GraphQL rules inspect individual HTTP requests. /// After an upgrade the relay would copy frames that no rule of these /// protocols evaluates, so they never upgrade; GraphQL over WebSocket is /// served by separate `protocol: websocket` endpoints with GraphQL operation -/// rules. REST and WebSocket endpoints relay allowed upgrades, and SQL +/// rules. WebSocket endpoints relay allowed upgrades, and SQL /// endpoints keep their existing upgrade behavior. The match is exhaustive so /// a new protocol must choose. pub(crate) fn upgrade_refusal_for_protocol( @@ -2456,7 +2457,8 @@ pub(crate) fn upgrade_refusal_for_protocol( match protocol { L7Protocol::JsonRpc | L7Protocol::Mcp => Some(UNSUPPORTED_JSONRPC_UPGRADE_DETAIL), L7Protocol::Graphql => Some(UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL), - L7Protocol::Rest | L7Protocol::Websocket | L7Protocol::Sql => None, + L7Protocol::Rest => Some(UNSUPPORTED_REST_UPGRADE_DETAIL), + L7Protocol::Websocket | L7Protocol::Sql => None, } } @@ -9504,16 +9506,10 @@ mod tests { let websocket = format!( "GET /ws HTTP/1.1\r\nHost: example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {VALID_WS_KEY}\r\nSec-WebSocket-Version: 13\r\n\r\n" ); - for protocol in [ - crate::l7::L7Protocol::Rest, - crate::l7::L7Protocol::Websocket, - ] { - assert_eq!( - unsupported_upgrade_detail(websocket.as_bytes(), protocol), - None, - "{protocol:?}" - ); - } + assert_eq!( + unsupported_upgrade_detail(websocket.as_bytes(), crate::l7::L7Protocol::Websocket), + None, + ); // Streamable HTTP requests, a look-alike header name, and a stray // `Connection: upgrade` without an `Upgrade` header stay allowed; @@ -9526,6 +9522,7 @@ mod tests { ]; for raw in ordinary { for protocol in [ + crate::l7::L7Protocol::Rest, crate::l7::L7Protocol::JsonRpc, crate::l7::L7Protocol::Mcp, crate::l7::L7Protocol::Graphql, @@ -9576,7 +9573,11 @@ mod tests { upgrade_refusal_for_protocol(L7Protocol::Graphql), Some(UNSUPPORTED_GRAPHQL_UPGRADE_DETAIL) ); - for protocol in [L7Protocol::Rest, L7Protocol::Websocket, L7Protocol::Sql] { + assert_eq!( + upgrade_refusal_for_protocol(L7Protocol::Rest), + Some(UNSUPPORTED_REST_UPGRADE_DETAIL) + ); + for protocol in [L7Protocol::Websocket, L7Protocol::Sql] { assert_eq!(upgrade_refusal_for_protocol(protocol), None, "{protocol:?}"); } } @@ -9614,6 +9615,7 @@ mod tests { "fixture must be an upgrade to the relay: {raw}" ); for protocol in [ + crate::l7::L7Protocol::Rest, crate::l7::L7Protocol::JsonRpc, crate::l7::L7Protocol::Mcp, crate::l7::L7Protocol::Graphql, diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index e6ec6ead5f..257bd3ce2e 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -4566,6 +4566,10 @@ process: let mut policy = valid.clone(); policy["version"] = 1.into(); policy["network_policies"]["admin"]["endpoints"][0][field] = control; + if field == "websocket_credential_rewrite" { + policy["network_policies"]["admin"]["endpoints"][0]["protocol"] = + "websocket".into(); + } openshell_policy::parse_sandbox_policy(&policy.to_string()).expect("typed valid twin"); OpaEngine::from_strings(TEST_POLICY, &policy.to_string()).expect("raw valid twin"); policy["network_policies"]["admin"]["endpoints"][0][field] = invalid; @@ -8277,7 +8281,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "gateway.example.com".to_string(), port: 443, - protocol: "rest".to_string(), + protocol: "websocket".to_string(), enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, access: openshell_core::proto::NetworkAccessPreset::Full as i32, websocket_credential_rewrite: true, diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index 66405e5e5b..d7dec792a6 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -408,6 +408,11 @@ instead of broadening the range when an address changes. ### Allow WebSocket Messages +REST endpoints refuse requests carrying an `Upgrade` header with `403 Forbidden` +in both `enforce` and `audit` mode. To migrate a REST endpoint that previously +relayed WebSocket traffic, configure a separate `protocol: websocket` endpoint +on its own path or port with explicit upgrade and message rules. + Use `protocol: websocket` to control WebSocket connections and the messages that clients send over them. This template requires `/usr/bin/node` and a WebSocket service you control. It permits the `/v1/realtime` upgrade and client diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 15a2bc7b0c..67d9090e60 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -158,7 +158,7 @@ the gateway host. | Field | Type | Default | Description | |---|---|---|---| -| `protocol` | string | None | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection, or `tcp` for a native TCP connection. `graphql`, `mcp`, and `json-rpc` endpoints refuse requests that carry an `Upgrade` header with `403`. Refer to [Connection and Request Checks](/how-it-works/policies/network-rules#connection-and-request-checks). | +| `protocol` | string | None | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection, or `tcp` for a native TCP connection. `rest`, `graphql`, `mcp`, and `json-rpc` endpoints refuse requests that carry an `Upgrade` header with `403`. Refer to [Connection and Request Checks](/how-it-works/policies/network-rules#connection-and-request-checks). | | `tls` | string | Automatic | `skip` relays traffic without terminating TLS, so OpenShell cannot inspect it. Do not use it with a request protocol. | | `enforcement` | string | `audit` | `enforce` blocks requests that break the endpoint's rules. `audit` logs them and allows the request. | | `access` | string | None | Access preset: `read-only`, `read-write`, or `full`. Refer to [Access Presets](#access-presets). | @@ -172,7 +172,7 @@ the gateway host. |---|---|---|---| | `credential_binding.provider` | string | None | Binds the static credentials of an attached provider whose profile defines no endpoints. Valid only in a sandbox policy. | | `request_body_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in REST bodies, including those with declared HTTP placement. | -| `websocket_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in client WebSocket text messages, including those with declared HTTP placement. | +| `websocket_credential_rewrite` | bool | `false` | Replaces ordinary static credential placeholders in client WebSocket text messages, including those with declared HTTP placement. Requires `protocol: websocket`. | | `allow_uninspected_credentials` | bool | `false` | Allows a provider-credentialed endpoint to omit request inspection or use `tls: skip`. | | `credential_signing` | string | None | AWS request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. | | `signing_service` | string | None | AWS service name, such as `bedrock` or `s3`. Required with `credential_signing`. | diff --git a/docs/how-it-works/providers/overview.mdx b/docs/how-it-works/providers/overview.mdx index a5299802a7..aba6269546 100644 --- a/docs/how-it-works/providers/overview.mdx +++ b/docs/how-it-works/providers/overview.mdx @@ -490,7 +490,8 @@ The proxy does not rewrite cookies, response content, unsupported request bodies, or WebSocket binary frames. Explicit endpoint policies can enable credential rewriting in request bodies -or WebSocket text messages. These flags do not authorize arbitrary query +or WebSocket text messages. WebSocket text message rewriting requires +`protocol: websocket`. These flags do not authorize arbitrary query parameters, path slots, or unrelated headers. See [Profiles](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding) for placement configuration and upgrade requirements. diff --git a/e2e/rust/tests/forward_proxy_l7_bypass.rs b/e2e/rust/tests/forward_proxy_l7_bypass.rs index d726b84721..f274f20e93 100644 --- a/e2e/rust/tests/forward_proxy_l7_bypass.rs +++ b/e2e/rust/tests/forward_proxy_l7_bypass.rs @@ -19,6 +19,13 @@ async fn start_test_server(alias: &str) -> Result { class Handler(BaseHTTPRequestHandler): def do_GET(self): + if self.headers.get("Upgrade"): + self.send_response(101) + self.send_header("Connection", "Upgrade") + self.send_header("Upgrade", "websocket") + self.send_header("Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") + self.end_headers() + return self.send_response(200) self.end_headers() self.wfile.write(b'{"ok":true}') @@ -178,3 +185,55 @@ except Exception as e: guard.create_output ); } + +/// A permitted GET cannot switch a REST endpoint into an uninspected tunnel. +#[tokio::test] +async fn rest_upgrade_is_refused_in_enforce_and_audit_modes() { + let server = start_test_server("rest-upgrade.openshell.test") + .await + .expect("start backend"); + for enforcement in ["enforce", "audit"] { + let source = write_policy_with_l7_rules(&server.host, server.port).unwrap(); + let policy = std::fs::read_to_string(source.path()).unwrap().replace( + "enforcement: enforce", + &format!("enforcement: {enforcement}"), + ); + let mut file = NamedTempFile::new().unwrap(); + file.write_all(policy.as_bytes()).unwrap(); + file.flush().unwrap(); + let policy_path = file.path().to_str().unwrap(); + let script = format!( + r#" +import socket +for target in ["/allowed", "http://{host}:{port}/allowed"]: + s = socket.create_connection(("{host}", {port}), timeout=15) + request = ("GET " + target + " HTTP/1.1\r\nHost: {host}:{port}\r\n" + "Connection: Upgrade\r\nUpgrade: websocket\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n") + s.sendall(request.encode()) + response = b"" + while True: + chunk = s.recv(4096) + if not chunk: + break + response += chunk + s.close() + assert response.startswith(b"HTTP/1.1 403"), response + assert b"unsupported_l7_protocol" in response, response + assert b"REST endpoints" in response, response +print("REST_UPGRADE_REFUSED") +"#, + host = server.host, + port = server.port + ); + let guard = + SandboxGuard::create(&["--policy", policy_path, "--", "python3", "-c", &script]) + .await + .expect("sandbox create"); + assert!( + guard.create_output.contains("REST_UPGRADE_REFUSED"), + "{enforcement}: {}", + guard.create_output + ); + } +} diff --git a/skills/generate-sandbox-policy/SKILL.md b/skills/generate-sandbox-policy/SKILL.md index 9dcb5d9d8d..864e9d61f3 100644 --- a/skills/generate-sandbox-policy/SKILL.md +++ b/skills/generate-sandbox-policy/SKILL.md @@ -246,6 +246,7 @@ Only needed for the **Moderate** and **Full** tiers. Translate API path paramete | All sub-paths under `/api/v1/` | `/api/v1/**` | In REST rule paths, `*` stops at `/`, while a whole-segment `**` crosses `/`. +REST endpoints refuse HTTP upgrades in both enforce and audit modes. For WebSocket traffic, use a separate `protocol: websocket` endpoint with explicit upgrade and message rules; `websocket_credential_rewrite` requires that protocol. For example, `/api/v1/**` covers descendants but not `/api/v1` itself; add an explicit rule for that path when needed. `?` matches one character except `/`, and bracket classes such as `[0-9]` are supported. From cc4acdcfdeda9fdeeb0fe94a537d47ad8414eb3d Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Fri, 6 Nov 2026 18:15:19 +0000 Subject: [PATCH 22/31] fix(isolation): enforce workload identity invariants on deserialize (#5192) ResolvedWorkloadIdentity derives Deserialize with public fields, so a value decoded from a serialized descriptor (for example the supervisor's SandboxRuntimeDescriptor payload) bypassed the non-root, non-empty, and sorted-unique-group invariants that new() enforces. Route deserialization through a private raw mirror type and a TryFrom impl that delegates to new(), so invalid identities fail to decode and supplementary groups are normalized on the way in. The wire format for valid values is unchanged. Signed-off-by: Emilien Macchi --- .../src/contract.rs | 32 +++++ .../tests/backend_conformance.rs | 109 ++++++++++++++++++ 2 files changed, 141 insertions(+) diff --git a/crates/openshell-isolation-interface/src/contract.rs b/crates/openshell-isolation-interface/src/contract.rs index de2a01975f..bd328fc0a1 100644 --- a/crates/openshell-isolation-interface/src/contract.rs +++ b/crates/openshell-isolation-interface/src/contract.rs @@ -141,7 +141,13 @@ pub struct BackendDescriptor { } /// Exact non-root identity selected before the immutable workload is created. +/// +/// Validation and normalization run in [`ResolvedWorkloadIdentity::new`] and +/// again when a value is decoded from a serialized descriptor: deserialization +/// routes through the same constructor, so an invalid identity fails to decode +/// instead of bypassing the contract. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "RawResolvedWorkloadIdentity")] pub struct ResolvedWorkloadIdentity { /// Effective and real user ID used by sandbox and all workload children. pub uid: u32, @@ -187,6 +193,32 @@ impl ResolvedWorkloadIdentity { } } +/// Unvalidated wire shape of [`ResolvedWorkloadIdentity`], existing only so +/// serde decoding can route through [`ResolvedWorkloadIdentity::new`]. +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct RawResolvedWorkloadIdentity { + uid: u32, + gid: u32, + supplementary_gids: Vec, + source: String, + resource_digest: String, +} + +impl TryFrom for ResolvedWorkloadIdentity { + type Error = BackendError; + + fn try_from(raw: RawResolvedWorkloadIdentity) -> Result { + Self::new( + raw.uid, + raw.gid, + raw.supplementary_gids, + raw.source, + raw.resource_digest, + ) + } +} + /// The trusted sandbox context, constructed by trusted common code after the /// control plane assigns the resource to the admitted sandbox. /// diff --git a/crates/openshell-isolation-interface/tests/backend_conformance.rs b/crates/openshell-isolation-interface/tests/backend_conformance.rs index e34e6215bd..9928b02073 100644 --- a/crates/openshell-isolation-interface/tests/backend_conformance.rs +++ b/crates/openshell-isolation-interface/tests/backend_conformance.rs @@ -77,6 +77,115 @@ fn workload_identity_rejects_root_and_normalizes_groups() { assert_eq!(identity.supplementary_gids, vec![1002, 1003]); } +#[test] +fn workload_identity_deserialization_enforces_new_invariants() { + fn payload(uid: u32, gid: u32, gids: &[u32], source: &str, digest: &str) -> serde_json::Value { + serde_json::json!({ + "uid": uid, + "gid": gid, + "supplementary_gids": gids, + "source": source, + "resource_digest": digest, + }) + } + + assert!( + serde_json::from_value::(payload( + 0, + 1000, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 0, + &[], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[0], + "policy", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + "", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", "")) + .is_err() + ); + assert!( + serde_json::from_value::(payload( + 1000, + 1000, + &[], + " ", + "sha256:test" + )) + .is_err() + ); + assert!( + serde_json::from_value::(payload(1000, 1000, &[], "policy", " ")) + .is_err() + ); + + let identity = workload_identity(); + let encoded = serde_json::to_value(&identity).unwrap(); + assert_eq!( + serde_json::from_value::(encoded).unwrap(), + identity + ); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1003, 1001, 1002, 1003], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001, 1002, 1003]); + + let decoded = serde_json::from_value::(payload( + 1000, + 1000, + &[1000, 1001], + "policy", + "sha256:test", + )) + .unwrap(); + assert_eq!(decoded.supplementary_gids, vec![1001]); +} + +#[test] +fn boundary_confirmation_rejects_invalid_embedded_identity() { + let encoded = serde_json::to_value(confirmation()).unwrap(); + serde_json::from_value::(encoded.clone()).unwrap(); + + let mut invalid = encoded; + invalid["identity"]["uid"] = serde_json::json!(0); + assert!(serde_json::from_value::(invalid).is_err()); +} + #[test] fn confirmation_rejects_missing_generation_or_identity_drift() { let expected = workload_identity(); From 2856eef417c50de6375852e8175ab0a6dc0011b8 Mon Sep 17 00:00:00 2001 From: Divesh <47188680+FrostGod@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:47:28 +0000 Subject: [PATCH 23/31] fix(server): bound Postgres pool waits and retry startup connect (#5244) * fix(server): bound Postgres pool waits and retry startup connect The gateway's data pool waited sqlx's default 30 seconds for a connection and connected only once at startup. An unreachable PostgreSQL therefore held every request for 30 seconds before failing with INTERNAL, and a gateway that started during the outage exited and crash-looped. - Bound data-pool acquires at 5 seconds and report a pool timeout as a retryable UNAVAILABLE (DATABASE_UNAVAILABLE) from persistence error mapping, including the workspace and membership lookups. - Retry an unreachable database at startup with capped backoff. The first failure binds the health listener, so /healthz answers and /readyz stays not ready, and starts listening for SIGTERM and SIGINT. A reachable database at startup keeps the previous order exactly. - Keep retrying startup endpoint-status reconciliation on pool timeouts. Closes #5238 Signed-off-by: divesh * chore(server): trim Postgres startup retry comments Refs #5238 Signed-off-by: divesh * fix(server): retry only unreachable-database errors at startup Narrow the startup retry to connection errors that mean PostgreSQL is not serving yet, and apply it to migrations, so a read-only standby or a connection lost during migration is retried while a bad host, credentials, or certificate still fails fast. Document DATABASE_UNAVAILABLE. Signed-off-by: divesh --------- Signed-off-by: divesh --- .../src/auth/workspace_authz.rs | 2 +- crates/openshell-server/src/grpc/mod.rs | 42 ++- .../src/grpc/policy/endpoint_status.rs | 8 +- crates/openshell-server/src/grpc/workspace.rs | 2 +- crates/openshell-server/src/lib.rs | 303 ++++++++++++++++-- .../openshell-server/src/persistence/mod.rs | 53 ++- .../src/persistence/mutation_lock_pg_tests.rs | 73 ++++- .../src/persistence/postgres.rs | 115 ++++++- crates/openshell-server/src/readiness.rs | 11 + docs/how-it-works/gateways/configuration.mdx | 2 +- docs/kubernetes/setup.mdx | 2 + docs/sdk/api-errors.mdx | 1 + skills/debug-openshell-cluster/SKILL.md | 2 + 13 files changed, 556 insertions(+), 60 deletions(-) diff --git a/crates/openshell-server/src/auth/workspace_authz.rs b/crates/openshell-server/src/auth/workspace_authz.rs index bc5cac8778..035d8aa354 100644 --- a/crates/openshell-server/src/auth/workspace_authz.rs +++ b/crates/openshell-server/src/auth/workspace_authz.rs @@ -156,7 +156,7 @@ pub async fn authorize_workspace( &user.identity.subject, ) .await - .map_err(|e| Status::internal(format!("membership lookup failed: {e}")))?; + .map_err(|e| crate::grpc::persistence_error_to_status(e, "membership lookup"))?; let Some(member) = member else { let workspace_arg = shell_quote_for_hint(&workspace); diff --git a/crates/openshell-server/src/grpc/mod.rs b/crates/openshell-server/src/grpc/mod.rs index 2efcb9873e..69208348de 100644 --- a/crates/openshell-server/src/grpc/mod.rs +++ b/crates/openshell-server/src/grpc/mod.rs @@ -84,7 +84,8 @@ use crate::ServerState; /// CAS conflicts (optimistic concurrency failures) are mapped to `ABORTED` /// to signal that the client should retry with fresh data. Mutation lock /// timeouts are mapped to `UNAVAILABLE` with a retry delay: the mutation lock -/// was not acquired, so this request's guarded writes did not run. Other +/// was not acquired, so this request's guarded writes did not run. So is a +/// database without a free connection, which ran no statement. Other /// persistence errors are mapped to `INTERNAL`. pub fn persistence_error_to_status( err: crate::persistence::PersistenceError, @@ -108,6 +109,11 @@ pub fn persistence_error_to_status( format!("{operation} timed out waiting for a concurrent mutation; retry the request"), std::time::Duration::from_secs(1), ), + PersistenceError::Unavailable(_) => openshell_core::rpc_error::unavailable( + "DATABASE_UNAVAILABLE", + format!("{operation} failed: the database is unavailable; retry the request"), + std::time::Duration::from_secs(1), + ), other => Status::internal(format!("{operation} failed: {other}")), } } @@ -1367,6 +1373,40 @@ mod tests { ); } + #[test] + fn persistence_error_to_status_maps_database_unavailable_to_unavailable() { + let status = persistence_error_to_status( + crate::persistence::PersistenceError::Unavailable( + "pool timed out while waiting for an open connection".into(), + ), + "workspace lookup", + ); + + assert_eq!(status.code(), tonic::Code::Unavailable); + assert_eq!( + status.message(), + "workspace lookup failed: the database is unavailable; retry the request" + ); + let details = openshell_core::rpc_error::decode_details(&status).expect("error details"); + assert_eq!( + details.error_info().expect("error info").reason, + "DATABASE_UNAVAILABLE" + ); + assert_eq!( + details.retry_info().expect("retry info").retry_delay, + Some(std::time::Duration::from_secs(1)) + ); + assert!( + !openshell_core::replica_routing::retry_unrouted( + Some(&tonic::metadata::AsciiMetadataValue::from_static( + "openshell-0" + )), + &status, + ), + "a gateway that answered must not look like an unrouted edge failure" + ); + } + #[test] fn public_resource_capabilities_preserves_absence() { let absent: Option = None; diff --git a/crates/openshell-server/src/grpc/policy/endpoint_status.rs b/crates/openshell-server/src/grpc/policy/endpoint_status.rs index 88efafada1..6d35131cc5 100644 --- a/crates/openshell-server/src/grpc/policy/endpoint_status.rs +++ b/crates/openshell-server/src/grpc/policy/endpoint_status.rs @@ -738,9 +738,11 @@ async fn invalidate_sandbox_endpoint_status_once( // Lifecycle writers on other replicas take no distributed guard, and a // delete between the re-read and the write surfaces as a database // error. Retry with a fresh read after this guard drops. - Err(error @ (PersistenceError::Conflict { .. } | PersistenceError::Database(_))) => { - Ok(StartupAttempt::Retry(error)) - } + Err( + error @ (PersistenceError::Conflict { .. } + | PersistenceError::Database(_) + | PersistenceError::Unavailable(_)), + ) => Ok(StartupAttempt::Retry(error)), Err(error) => Err(super::super::persistence_error_to_status( error, "invalidate tool server endpoint status during gateway startup", diff --git a/crates/openshell-server/src/grpc/workspace.rs b/crates/openshell-server/src/grpc/workspace.rs index 56156f7d3f..9f3be64703 100644 --- a/crates/openshell-server/src/grpc/workspace.rs +++ b/crates/openshell-server/src/grpc/workspace.rs @@ -130,7 +130,7 @@ pub async fn resolve_workspace( let ws: Option = store .get_message_by_name("", &name) .await - .map_err(|e| Status::internal(format!("workspace lookup failed: {e}")))?; + .map_err(|e| super::persistence_error_to_status(e, "workspace lookup"))?; match ws { Some(ws) => { diff --git a/crates/openshell-server/src/lib.rs b/crates/openshell-server/src/lib.rs index 02c1ba16b3..140f372347 100644 --- a/crates/openshell-server/src/lib.rs +++ b/crates/openshell-server/src/lib.rs @@ -827,14 +827,21 @@ pub(crate) async fn run_server( .map_err(|error| Error::config(format!("middleware registration failed: {error}")))?, ); - let store = Arc::new( - Store::connect_with_pool_sizes( - database_url, - config.database_max_connections, - config.database_lock_max_connections, - ) - .await?, - ); + let shutdown = shutdown_signal(); + tokio::pin!(shutdown); + let mut health_listener = HealthListener::new(config.health_bind_address); + let Some(store) = connect_store( + &config, + database_url, + &mut health_listener, + shutdown.as_mut(), + ) + .await? + else { + info!("Shutdown signal received while waiting for the database; stopping gateway"); + return Ok(()); + }; + let store = Arc::new(store); let credentials = credentials::CredentialRuntime::from_config_file_with_store( &config, config_file.as_ref(), @@ -1153,26 +1160,7 @@ pub(crate) async fn run_server( // Create the multiplexed service let service = MultiplexService::new(state.clone()); - // Bind the unauthenticated health endpoint on a separate port when configured. - if let Some(health_bind_address) = config.health_bind_address { - let health_listener = TcpListener::bind(health_bind_address).await.map_err(|e| { - Error::transport(format!( - "failed to bind health port {health_bind_address}: {e}" - )) - })?; - info!(address = %health_bind_address, "Health server listening"); - // `health_router` returns immediately; the listener serves - // `Initializing → 503` until the background monitor publishes the - // first real probe outcome, so the endpoint is always responsive. - let router = health_router(store.clone()); - tokio::spawn(async move { - if let Err(e) = axum::serve(health_listener, router.into_make_service()).await { - error!("Health server error: {e}"); - } - }); - } else { - info!("Health server disabled"); - } + health_listener.serve(store.clone()).await?; // Bind the Prometheus metrics endpoint on a dedicated port when configured. if let Some((metrics_bind_address, prometheus_handle)) = metrics_endpoint { @@ -1311,7 +1299,7 @@ pub(crate) async fn run_server( provider_profile_sources::spawn_refresh(state.clone()); provider_refresh::spawn_refresh_worker(state.clone(), Duration::from_mins(1)); - shutdown_signal().await; + shutdown.await; info!("Shutdown signal received; stopping gateway"); state.gateway_shutting_down.store(true, Ordering::Release); state.supervisor_sessions.close_admission(); @@ -1607,6 +1595,112 @@ fn spawn_gateway_connection( } } +/// First delay between attempts to reach an unreachable database at startup. +const DATABASE_CONNECT_RETRY_INITIAL_DELAY: Duration = Duration::from_secs(1); + +/// Longest delay between attempts to reach an unreachable database at startup. +const DATABASE_CONNECT_RETRY_MAX_DELAY: Duration = Duration::from_secs(2); + +/// Connect to the database, retrying while it is unreachable. +/// +/// The first failed attempt binds the health endpoint and starts listening +/// for `shutdown`; a gateway running as PID 1 ignores SIGTERM until then. +/// Returns `None` if `shutdown` completes first. +async fn connect_store( + config: &Config, + database_url: &str, + health_listener: &mut HealthListener, + mut shutdown: std::pin::Pin<&mut impl Future>, +) -> Result> { + let mut delay = DATABASE_CONNECT_RETRY_INITIAL_DELAY; + let mut waiting = false; + loop { + let attempt = Store::try_connect_with_pool_sizes( + database_url, + config.database_max_connections, + config.database_lock_max_connections, + ); + let result = if waiting { + tokio::select! { + () = &mut shutdown => return Ok(None), + result = attempt => result?, + } + } else { + attempt.await? + }; + let error = match result { + Ok(store) => return Ok(Some(store)), + Err(error @ persistence::PersistenceError::Unavailable(_)) => error, + Err(error) => return Err(Error::execution(error.to_string())), + }; + if !waiting { + health_listener.bind().await?; + waiting = true; + } + warn!( + error = %error, + retry_in_ms = u64::try_from(delay.as_millis()).unwrap_or(u64::MAX), + "database unreachable; retrying" + ); + tokio::select! { + () = &mut shutdown => return Ok(None), + () = tokio::time::sleep(delay) => {} + } + delay = (delay * 2).min(DATABASE_CONNECT_RETRY_MAX_DELAY); + } +} + +/// The unauthenticated health endpoint, served on its own port when +/// configured. +struct HealthListener { + address: Option, + state: Option>, +} + +impl HealthListener { + const fn new(address: Option) -> Self { + Self { + address, + state: None, + } + } + + /// Start serving probes, with `/readyz` reporting `Initializing` until + /// [`Self::serve`] publishes the database's health. + async fn bind(&mut self) -> Result<()> { + let Some(address) = self.address else { + return Ok(()); + }; + if self.state.is_some() { + return Ok(()); + } + let listener = TcpListener::bind(address) + .await + .map_err(|e| Error::transport(format!("failed to bind health port {address}: {e}")))?; + info!(address = %address, "Health server listening"); + let (sender, receiver) = watch::channel(readiness::HealthState::Initializing); + let router = http::health_router_from_receiver(receiver); + tokio::spawn(async move { + if let Err(e) = axum::serve(listener, router.into_make_service()).await { + error!("Health server error: {e}"); + } + }); + self.state = Some(sender); + Ok(()) + } + + /// Bind if not yet serving and publish `store`'s health from now on. + async fn serve(mut self, store: Arc) -> Result<()> { + self.bind().await?; + if let Some(sender) = self.state { + readiness::DatabaseHealthMonitor::spawn_into(store, sender); + } else { + info!("Health server disabled"); + } + Ok(()) + } +} + async fn shutdown_signal() { #[cfg(unix)] { @@ -3978,6 +4072,157 @@ mod tests { "persisted sandbox start must not run before the gateway listener is bound" ); } + + /// A loopback address where nothing listens until a test binds it. + async fn reserved_loopback_address() -> SocketAddr { + TcpListener::bind("127.0.0.1:0") + .await + .expect("reserve a loopback port") + .local_addr() + .expect("loopback address") + } + + /// Status code of `GET path`, or `None` when nothing answers. + async fn http_status(address: SocketAddr, path: &str) -> Option { + let mut stream = openshell_core::net::connect_tcp_nodelay_best_effort(&[address]) + .await + .ok()?; + stream + .write_all( + format!("GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") + .as_bytes(), + ) + .await + .ok()?; + let mut response = String::new(); + stream.read_to_string(&mut response).await.ok()?; + response.split_whitespace().nth(1)?.parse().ok() + } + + async fn wait_for_http_status(address: SocketAddr, path: &str, expected: u16) { + let deadline = tokio::time::Instant::now() + Duration::from_secs(20); + while http_status(address, path).await != Some(expected) { + assert!( + tokio::time::Instant::now() < deadline, + "{path} never returned {expected}" + ); + tokio::time::sleep(Duration::from_millis(100)).await; + } + } + + #[tokio::test] + async fn connect_store_serves_probes_while_the_database_is_unreachable() { + let url = crate::persistence::PostgresStore::refusing_url_for_tests().await; + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let (stop, stopped) = tokio::sync::oneshot::channel::<()>(); + let shutdown = async { + let _ = stopped.await; + }; + tokio::pin!(shutdown); + let config = Config::new(None); + + let probe = async { + wait_for_http_status(health_address, "/healthz", 200).await; + assert_eq!(http_status(health_address, "/readyz").await, Some(503)); + stop.send(()).expect("connect_store is still waiting"); + }; + let (result, ()) = tokio::join!( + super::connect_store(&config, &url, &mut health, shutdown.as_mut()), + probe + ); + + assert!( + result.expect("shutdown is not an error").is_none(), + "shutdown ends the wait without a store" + ); + } + + #[tokio::test] + async fn connect_store_returns_a_configuration_error_without_retrying() { + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let shutdown = std::future::pending::<()>(); + tokio::pin!(shutdown); + + let result = tokio::time::timeout( + Duration::from_secs(5), + super::connect_store( + &Config::new(None), + "postgres://openshell@127.0.0.1:1/openshell?sslmode=invalid", + &mut health, + shutdown.as_mut(), + ), + ) + .await + .expect("a configuration error is not retried"); + + assert!(result.is_err()); + assert_eq!(http_status(health_address, "/healthz").await, None); + } + + #[tokio::test] + #[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] + async fn postgres_connect_store_retries_until_the_database_answers() { + let schema = crate::persistence::test_postgres::TestSchema::create("startup").await; + let upstream = url::Url::parse(schema.url()).expect("parse the PostgreSQL URL"); + let upstream_addresses: Vec = tokio::net::lookup_host(( + upstream.host_str().expect("TCP PostgreSQL host"), + upstream.port().unwrap_or(5432), + )) + .await + .expect("resolve PostgreSQL") + .collect(); + let late_address = reserved_loopback_address().await; + let mut url = upstream.clone(); + url.set_host(Some("127.0.0.1")).expect("set host"); + url.set_port(Some(late_address.port())).expect("set port"); + let health_address = reserved_loopback_address().await; + let mut health = super::HealthListener::new(Some(health_address)); + let shutdown = std::future::pending::<()>(); + tokio::pin!(shutdown); + let config = Config::new(None); + + // PostgreSQL becomes reachable only after the gateway reports that it + // is waiting. + let late_database = async { + wait_for_http_status(health_address, "/healthz", 200).await; + assert_eq!(http_status(health_address, "/readyz").await, Some(503)); + let listener = TcpListener::bind(late_address) + .await + .expect("bind the late PostgreSQL address"); + loop { + let (mut client, _) = listener.accept().await.expect("accept"); + openshell_core::net::set_tcp_nodelay_best_effort(&client); + let mut upstream = + openshell_core::net::connect_tcp_nodelay_best_effort(&upstream_addresses) + .await + .expect("connect to PostgreSQL"); + tokio::spawn(async move { + let _ = tokio::io::copy_bidirectional(&mut client, &mut upstream).await; + }); + } + }; + let store = tokio::select! { + result = tokio::time::timeout( + Duration::from_mins(1), + super::connect_store(&config, url.as_str(), &mut health, shutdown.as_mut()), + ) => result + .expect("connect_store finishes once PostgreSQL answers") + .expect("connect") + .expect("no shutdown"), + () = late_database => unreachable!("the forwarder runs until the store connects"), + }; + store.ping().await.expect("ping the connected store"); + + health + .serve(Arc::new(store)) + .await + .expect("keep serving the bound health listener"); + wait_for_http_status(health_address, "/readyz", 200).await; + + schema.drop_schema().await; + } } #[cfg(test)] diff --git a/crates/openshell-server/src/persistence/mod.rs b/crates/openshell-server/src/persistence/mod.rs index 30280b96bb..d61451604d 100644 --- a/crates/openshell-server/src/persistence/mod.rs +++ b/crates/openshell-server/src/persistence/mod.rs @@ -69,6 +69,11 @@ pub enum PersistenceError { /// request's guarded writes did not run; the operation is safe to retry. #[error("mutation lock timeout: {0}")] LockTimeout(String), + /// The database is not serving. At request time, no connection was + /// available before the pool's acquire timeout, so the failed statement + /// was never sent. + #[error("database unavailable: {0}")] + Unavailable(String), } impl PersistenceError { @@ -475,6 +480,22 @@ impl Store { max_connections: Option, lock_max_connections: Option, ) -> CoreResult { + Self::try_connect_with_pool_sizes(url, max_connections, lock_max_connections) + .await? + .map_err(|e| CoreError::execution(e.to_string())) + } + + /// One attempt of [`Self::connect_with_pool_sizes`]. + /// + /// Configuration errors fail the outer result. Connection and migration + /// errors keep their [`PersistenceError`] kind in the inner result, so a + /// caller can retry [`PersistenceError::Unavailable`] until the database + /// is reachable. + pub(crate) async fn try_connect_with_pool_sizes( + url: &str, + max_connections: Option, + lock_max_connections: Option, + ) -> CoreResult> { if url.starts_with("postgres://") || url.starts_with("postgresql://") { if let Some(configured) = max_connections && configured < postgres::MIN_MAX_CONNECTIONS @@ -486,28 +507,25 @@ impl Store { postgres::MIN_MAX_CONNECTIONS ))); } - let store = PostgresStore::connect(url, max_connections, lock_max_connections) - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - store - .migrate() - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - Ok(Self::Postgres(store)) + Ok(async { + let store = + PostgresStore::connect(url, max_connections, lock_max_connections).await?; + store.migrate().await?; + Ok(Self::Postgres(store)) + } + .await) } else if url.starts_with("sqlite:") { if lock_max_connections.is_some() { tracing::warn!( "ignoring the configured mutation lock pool ceiling: SQLite has no lock pool" ); } - let store = SqliteStore::connect(url, max_connections) - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - store - .migrate() - .await - .map_err(|e| CoreError::execution(e.to_string()))?; - Ok(Self::Sqlite(store)) + Ok(async { + let store = SqliteStore::connect(url, max_connections).await?; + store.migrate().await?; + Ok(Self::Sqlite(store)) + } + .await) } else { Err(CoreError::config(format!( "unsupported database URL scheme: {url}" @@ -1446,6 +1464,9 @@ pub fn current_time_ms() -> i64 { } fn map_db_error(error: &sqlx::Error) -> PersistenceError { + if matches!(error, sqlx::Error::PoolTimedOut) { + return PersistenceError::Unavailable(error.to_string()); + } if let sqlx::Error::Database(db) = error && db.is_unique_violation() { diff --git a/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs b/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs index 3bf37d1a33..4873925972 100644 --- a/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs +++ b/crates/openshell-server/src/persistence/mutation_lock_pg_tests.rs @@ -17,7 +17,7 @@ use super::mutation_lock::{ GLOBAL_MUTATION_LOCK_KEY, LOCK_CONNECTION_MIN_BUDGET, MUTATION_LOCK_POOL_MAX_CONNECTIONS, MUTATION_LOCK_TIMEOUT, MutationLockKey, MutationLockSet, }; -use super::postgres::LOCK_CONNECTION_RELEASE_TIMEOUT; +use super::postgres::{DATA_POOL_ACQUIRE_TIMEOUT, LOCK_CONNECTION_RELEASE_TIMEOUT}; use super::test_postgres::TestSchema; use super::{ DistributedMutationGuard, PersistenceError, PersistenceResult, PostgresStore, Store, @@ -1261,6 +1261,77 @@ async fn postgres_mutation_lock_connection_failure_is_not_a_lock_timeout() { fixture.finish(vec![saturated, unanswered]).await; } +/// A data-pool caller whose `PostgreSQL` stops answering fails as retryable +/// within the acquire timeout, and the next caller after it answers again +/// gets a fresh connection. +#[tokio::test] +#[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] +async fn postgres_data_pool_fails_unavailable_while_postgres_does_not_answer() { + let schema = TestSchema::create("unavailable").await; + let proxy = StallingProxy::start(schema.url()).await; + let store = Store::Postgres( + PostgresStore::connect_with_lock_pool_size(&proxy.url, None, 1) + .await + .expect("connect a store through the proxy"), + ); + store.ping().await.expect("ping before the network stalls"); + + proxy.stalled.send_replace(true); + let started = Instant::now(); + let result = store.ping().await; + let waited = started.elapsed(); + assert!( + matches!(result, Err(PersistenceError::Unavailable(_))), + "{result:?}" + ); + assert!( + waited < DATA_POOL_ACQUIRE_TIMEOUT + Duration::from_secs(2), + "ping waited {waited:?}" + ); + + proxy.stalled.send_replace(false); + tokio::time::timeout(Duration::from_secs(5), store.ping()) + .await + .expect("ping after the network recovers is bounded") + .expect("ping after the network recovers"); + + drop(store); + schema.drop_schema().await; +} + +/// Migrating against a read-only server, as a standby before promotion, +/// fails as retryable rather than as a fatal migration error. +#[tokio::test] +#[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] +async fn postgres_migrate_on_a_read_only_server_fails_unavailable() { + let schema = TestSchema::create("read_only").await; + let mut url = url::Url::parse(schema.url()).expect("parse the PostgreSQL URL"); + let pairs: Vec<(String, String)> = url + .query_pairs() + .map(|(key, value)| { + let value = if key == "options" { + format!("{value} -cdefault_transaction_read_only=on") + } else { + value.into_owned() + }; + (key.into_owned(), value) + }) + .collect(); + url.query_pairs_mut().clear().extend_pairs(pairs); + let store = PostgresStore::connect(url.as_str(), None, None) + .await + .expect("connect to the read-only server"); + + let result = store.migrate().await; + assert!( + matches!(result, Err(PersistenceError::Unavailable(_))), + "{result:?}" + ); + + drop(store); + schema.drop_schema().await; +} + #[tokio::test] #[ignore = "requires a disposable PostgreSQL database in OPENSHELL_TEST_POSTGRES_URL; run mise run test:rust:postgres"] async fn postgres_mutation_lock_unrelated_sandboxes_outpace_global_serialization() { diff --git a/crates/openshell-server/src/persistence/postgres.rs b/crates/openshell-server/src/persistence/postgres.rs index ca070bedd6..45511726fd 100644 --- a/crates/openshell-server/src/persistence/postgres.rs +++ b/crates/openshell-server/src/persistence/postgres.rs @@ -275,6 +275,46 @@ pub const DEFAULT_MAX_CONNECTIONS: u32 = 10; /// wait on itself until the acquire times out. pub const MIN_MAX_CONNECTIONS: u32 = 2; +/// How long a data-pool caller waits for a connection, including opening one. +pub(super) const DATA_POOL_ACQUIRE_TIMEOUT: Duration = Duration::from_secs(5); + +/// Whether a connect or migration failure means `PostgreSQL` is not serving +/// yet, so startup retries it. A bad host, credentials, or certificate stays +/// fatal. +fn is_unavailable_at_startup(error: &sqlx::Error) -> bool { + use std::io::ErrorKind; + match error { + sqlx::Error::PoolTimedOut => true, + sqlx::Error::Io(error) => matches!( + error.kind(), + ErrorKind::ConnectionRefused + | ErrorKind::ConnectionReset + | ErrorKind::ConnectionAborted + | ErrorKind::NotConnected + | ErrorKind::BrokenPipe + | ErrorKind::TimedOut + | ErrorKind::UnexpectedEof + | ErrorKind::HostUnreachable + | ErrorKind::NetworkUnreachable + ), + // read_only_sql_transaction, too_many_connections, admin_shutdown, + // crash_shutdown, cannot_connect_now. + sqlx::Error::Database(error) => matches!( + error.code().as_deref(), + Some("25006" | "53300" | "57P01" | "57P02" | "57P03") + ), + _ => false, + } +} + +fn map_startup_error(error: &sqlx::Error) -> PersistenceError { + if is_unavailable_at_startup(error) { + PersistenceError::Unavailable(error.to_string()) + } else { + map_db_error(error) + } +} + impl PostgresStore { /// Connect and size the data and lock pools, falling back to /// [`DEFAULT_MAX_CONNECTIONS`] and `MUTATION_LOCK_POOL_MAX_CONNECTIONS`. @@ -304,9 +344,10 @@ impl PostgresStore { ); let pool = PgPoolOptions::new() .max_connections(max_connections) + .acquire_timeout(DATA_POOL_ACQUIRE_TIMEOUT) .connect(url) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; let lock_pool = PgPoolOptions::new() .max_connections(lock_pool_size) .min_connections(0) @@ -351,22 +392,30 @@ impl PostgresStore { POSTGRES_MIGRATOR .run(&self.pool) .await - .map_err(|e| map_migrate_error(&e))?; + .map_err(|e| match &e { + sqlx::migrate::MigrateError::Execute(error) + | sqlx::migrate::MigrateError::ExecuteMigration(error, _) + if is_unavailable_at_startup(error) => + { + PersistenceError::Unavailable(e.to_string()) + } + _ => map_migrate_error(&e), + })?; self.migrate_legacy_time_payloads().await } async fn migrate_legacy_time_payloads(&self) -> PersistenceResult<()> { - let mut transaction = self.pool.begin().await.map_err(|e| map_db_error(&e))?; + let mut transaction = self.pool.begin().await.map_err(|e| map_startup_error(&e))?; // Serialize this application-level data migration across gateway replicas. sqlx::query("SELECT pg_advisory_xact_lock(3052)") .execute(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; let rows = sqlx::query("SELECT id, object_type, payload FROM objects ORDER BY id FOR UPDATE") .fetch_all(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; for row in rows { let id: String = row.try_get("id").map_err(|e| map_db_error(&e))?; @@ -384,11 +433,14 @@ impl PostgresStore { .bind(id) .execute(&mut *transaction) .await - .map_err(|e| map_db_error(&e))?; + .map_err(|e| map_startup_error(&e))?; } } - transaction.commit().await.map_err(|e| map_db_error(&e)) + transaction + .commit() + .await + .map_err(|e| map_startup_error(&e)) } /// Verify the database is reachable by acquiring a pooled connection @@ -2283,6 +2335,55 @@ impl PostgresStore { mod tests { use super::*; + #[tokio::test] + async fn connect_to_a_refusing_server_fails_unavailable_within_the_acquire_timeout() { + let url = PostgresStore::refusing_url_for_tests().await; + let started = std::time::Instant::now(); + + let error = PostgresStore::connect(&url, None, None) + .await + .expect_err("nothing listens on the port"); + + assert!( + matches!(error, PersistenceError::Unavailable(_)), + "{error:?}" + ); + assert!( + started.elapsed() < DATA_POOL_ACQUIRE_TIMEOUT + Duration::from_secs(3), + "connect waited {:?}", + started.elapsed() + ); + } + + #[test] + fn startup_retries_io_errors_only_while_postgres_does_not_answer() { + use std::io::ErrorKind; + let io = |kind: ErrorKind| map_startup_error(&sqlx::Error::Io(kind.into())); + for kind in [ + ErrorKind::ConnectionRefused, + ErrorKind::ConnectionReset, + ErrorKind::TimedOut, + ErrorKind::HostUnreachable, + ErrorKind::UnexpectedEof, + ] { + assert!( + matches!(io(kind), PersistenceError::Unavailable(_)), + "{kind:?}" + ); + } + // An unresolvable host, a missing certificate file, a failed TLS handshake. + for kind in [ + ErrorKind::Other, + ErrorKind::NotFound, + ErrorKind::InvalidData, + ] { + assert!( + matches!(io(kind), PersistenceError::Database(_)), + "{kind:?}" + ); + } + } + #[tokio::test] async fn lock_connection_timeout_is_contention_only_if_the_pool_filled_during_the_wait() { let store = PostgresStore::connect_lazy_for_tests( diff --git a/crates/openshell-server/src/readiness.rs b/crates/openshell-server/src/readiness.rs index 7ca21605a8..fed1ffd102 100644 --- a/crates/openshell-server/src/readiness.rs +++ b/crates/openshell-server/src/readiness.rs @@ -129,6 +129,17 @@ impl DatabaseHealthMonitor { Self { receiver: rx } } + /// Spawn a monitor with the production defaults that publishes to a + /// channel whose receivers already serve probes. + pub fn spawn_into(store: Arc, sender: watch::Sender) { + tokio::spawn(monitor_loop( + store, + sender, + DEFAULT_CHECK_INTERVAL, + DEFAULT_CHECK_TIMEOUT, + )); + } + /// Subscribe to state updates. /// /// Returned receivers always observe the latest value with no lock diff --git a/docs/how-it-works/gateways/configuration.mdx b/docs/how-it-works/gateways/configuration.mdx index 424efa6016..8d39d6bbaf 100644 --- a/docs/how-it-works/gateways/configuration.mdx +++ b/docs/how-it-works/gateways/configuration.mdx @@ -610,7 +610,7 @@ that the previous process has stopped, not that sharing a SQLite file enables HA ## Database Pool -`[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving. +`[openshell.gateway] database_max_connections` sets the connection ceiling for the gateway's database pool. One pool is shared by every database-backed RPC, so this value is also the gateway's ceiling on concurrent database work. When the pool is saturated, callers queue on connection acquisition and sqlx logs that the time to acquire a connection exceeded its slow threshold. The background readiness monitor draws from the same pool, so sustained saturation can push its database ping past the check timeout and flip `/readyz` to not-ready while the gateway is otherwise serving. On Postgres, a caller that gets no connection within 5 seconds fails instead of waiting for the database to answer, usually with a retryable `UNAVAILABLE` error whose reason is `DATABASE_UNAVAILABLE`. Omit the key to use the backend default, which is unchanged from earlier releases: diff --git a/docs/kubernetes/setup.mdx b/docs/kubernetes/setup.mdx index bbc223e32f..fd8d134108 100644 --- a/docs/kubernetes/setup.mdx +++ b/docs/kubernetes/setup.mdx @@ -566,6 +566,8 @@ The gateway exposes `/healthz` for process liveness and `/readyz` for dependency - `startupProbe` and `livenessProbe` use `/healthz`. - `readinessProbe` uses `/readyz`, which reflects the latest result of an in-process background database check. +A gateway that starts while its database is unreachable keeps retrying the connection. Meanwhile `/healthz` succeeds and `/readyz` reports not ready, so Kubernetes keeps the pod without routing traffic to it. + Each sandbox supervisor Pod uses a `tcpSocket` readiness probe on port 5501. The supervisor accepts connections on that port only while its gateway session is ready. If you add a default-deny ingress policy to sandbox namespaces, allow TCP port 5501 from the node so kubelet can reach supervisor Pods. ## Next Steps diff --git a/docs/sdk/api-errors.mdx b/docs/sdk/api-errors.mdx index 5d45369e68..7f11c9ea45 100644 --- a/docs/sdk/api-errors.mdx +++ b/docs/sdk/api-errors.mdx @@ -31,6 +31,7 @@ Recognized gateway reasons include the following. | `INVALID_ARGUMENT` | `INVALID_ARGUMENT` | Correct the fields listed in `BadRequest`. | | `RESOURCE_VERSION_CONFLICT` | `ABORTED` | Read the resource again and construct a new conditional write. `metadata.recovery` is `REFRESH_STATE`; `current_resource_version` is included when known. | | `PROFILE_SOURCE_UNAVAILABLE` | `UNAVAILABLE` | Retry a profile snapshot read after at least the supplied delay. | +| `DATABASE_UNAVAILABLE` | `UNAVAILABLE` | The gateway could not get a database connection within 5 seconds. Retry after at least the supplied delay. | | `MUTATION_LOCK_TIMEOUT` | `UNAVAILABLE` | The request waited more than 10 seconds for a concurrent mutation to release a sandbox, workspace, or gateway-wide lock. Retry after at least the supplied delay. A request that carried a `request_id` leaves its admission unresolved, like any other error, so a retry with that ID returns `REQUEST_OUTCOME_UNCERTAIN`. Observe resource state and reconcile effects before you start a new request with a new ID. | | `REQUEST_ID_PAYLOAD_MISMATCH` | `FAILED_PRECONDITION` | Keep the original payload for that request ID. Inspect the original operation before submitting a different one. | | `REQUEST_OUTCOME_UNCERTAIN` | `FAILED_PRECONDITION` | An attempt is admitted but has no confirmed replayable success. Observe resource state and reconcile effects. Do not switch to a new ID to bypass the admission. | diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index ec8c9dee6f..535a8daf26 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -1593,6 +1593,8 @@ credential failures. | Kubernetes sandbox pod stuck pending, workspace PVC unbound | Cluster has no default `StorageClass` and OpenShell does not set `storageClassName` on the workspace PVC (clusters with a default `StorageClass` bind fine without it) | `kubectl -n openshell describe pvc`; set `server.workspaceStorageClass` (gateway config `workspace_storage_class`) to a valid `StorageClass` | | Kubernetes gateway pod crash loops | Missing secret, bad DB URL, bad TLS config | `kubectl -n openshell logs deployment/openshell -c openshell-gateway` or `kubectl -n openshell logs statefulset/openshell -c openshell-gateway` | | Mutating RPCs return `UNAVAILABLE` with "timed out waiting for a concurrent mutation" | Advisory-lock contention, a full lock pool on one replica (4 connections unless `database_lock_max_connections` is set), a slow PostgreSQL, or older gateways still running during an upgrade | `openshell_server_mutation_lock_timeouts_total`, `openshell_server_mutation_lock_connections_in_use` against its capacity, the `detail` field of `mutation lock acquisition timed out` in gateway logs, `mutation lock held longer than the lock wait timeout` warnings and `openshell_server_mutation_lock_hold_seconds`, the `pg_locks` query in Step 6 | +| RPCs fail after about 5 seconds with `UNAVAILABLE` `DATABASE_UNAVAILABLE` or "database unavailable: pool timed out" | PostgreSQL unreachable or not answering, or every data-pool connection busy | `database health check` warnings and `/readyz` on the gateway, PostgreSQL logs, `database_max_connections` and the connection sizing in the High Availability guide | +| Kubernetes gateway pod Running but never Ready, logging `database unreachable; retrying` | The gateway started while PostgreSQL was unreachable and waits for it | PostgreSQL pod and Service endpoints, the host and port in the database URL, network policy between the gateway and PostgreSQL | | Mutating RPCs return `INTERNAL` with "could not open a mutation lock connection" | PostgreSQL out of connection slots, restarting, or unreachable | `openshell_server_mutation_lock_errors_total`, PostgreSQL logs, `SELECT count(*) FROM pg_stat_activity` against `max_connections`, `database_max_connections` and `database_lock_max_connections`, the connection sizing in the High Availability guide | | `helm upgrade` fails with an `autoscaling.*` message | HPA values invalid: missing `resources.requests` (or `resources.limits`), `maxReplicas` above 1 without `server.externalDbSecret` (or on a StatefulSet without `workload.allowMultiReplicaStatefulSet`), no metric target, or min/max out of order. "`minReplicas` and `maxReplicas` are not set" means `--reuse-values` kept a release without the chart's autoscaling defaults | Fix the values named in the error; upgrade with `--reset-then-reuse-values` instead of `--reuse-values` | | HPA shows `` targets | No metrics-server for CPU/memory, or the metrics adapter does not serve the custom metric | `kubectl -n openshell describe hpa openshell`, `kubectl get --raw /apis/custom.metrics.k8s.io/v1beta1` | From e4dd312f00385308d4fc7497894a05db7b1bbe93 Mon Sep 17 00:00:00 2001 From: Bryce Wilkinson Date: Fri, 6 Nov 2026 18:58:45 +0000 Subject: [PATCH 24/31] feat(helm): expose priorityClassName and topologySpreadConstraints (#2832) * feat(helm): expose priorityClassName and topologySpreadConstraints The gateway pod template exposes nodeSelector, affinity and tolerations but not priorityClassName or topologySpreadConstraints. Operators who need either have to fork the chart or mutate the rendered output out-of-band. Add two optional passthroughs: - priorityClassName: the gateway is a control-plane component. When it shares nodes with the workloads it serves, the default priority gives it no advantage under node pressure, so a local capacity crunch can evict it and widen into a fleet-wide outage. - topologySpreadConstraints: spread replicas across zones or nodes. Only affinity was available, which is a blunter tool for even spreading. Both go in the shared gatewayPodTemplate, so they apply identically to the statefulset and deployment workload shapes. Both render only when set; a default render is byte-identical to before this change. The data volume size/StorageClass passthroughs from the original revision are dropped: upstream now provides them as server.dataStorageSize and server.dataStorageClass. Signed-off-by: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> * test(helm): cover scheduling defaults and document gateway placement Signed-off-by: Taylor Mutch --------- Signed-off-by: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> Signed-off-by: Taylor Mutch Co-authored-by: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> Co-authored-by: Taylor Mutch --- deploy/helm/openshell/README.md | 2 + .../openshell/templates/_gateway-workload.tpl | 7 ++ .../tests/gateway_scheduling_test.yaml | 107 ++++++++++++++++++ deploy/helm/openshell/values.yaml | 9 ++ docs/kubernetes/setup.mdx | 2 + skills/debug-openshell-cluster/SKILL.md | 8 ++ 6 files changed, 135 insertions(+) create mode 100644 deploy/helm/openshell/tests/gateway_scheduling_test.yaml diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 58aa132eb6..bc6676b9f1 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -615,6 +615,7 @@ discovery endpoint or its TLS CA. | podLabels | object | `{}` | Extra labels to add to the gateway pod. | | podLifecycle.terminationGracePeriodSeconds | int | `30` | Maximum time, in seconds, Kubernetes waits for the gateway to exit before killing it. The gateway exits as soon as shutdown completes; the limit covers supervisor session cleanup and draining queued OCSF records. | | podSecurityContext.fsGroup | int | `1000` | fsGroup assigned to the gateway pod. | +| priorityClassName | string | `""` | PriorityClass for the gateway pod. The gateway is a control-plane component; when it shares nodes with the workloads it serves, the default priority gives it no advantage under node pressure. Empty = omit the field. | | probes.liveness.failureThreshold | int | `3` | Liveness probe failure threshold before the container is restarted. | | probes.liveness.initialDelaySeconds | int | `2` | Liveness probe initial delay, in seconds. | | probes.liveness.periodSeconds | int | `5` | Liveness probe period, in seconds. | @@ -758,6 +759,7 @@ discovery endpoint or its TLS CA. | supervisor.replicas | int | `1` | Shared supervisor pool members, used only when gatewayConfig selects supervisor_topology = "gateway". The gateway always coordinates the shared supervisor as a high-availability pool: when a member fails or leaves, another member takes over its sandboxes without restarting their workloads. 1 has no failover; use 2 or more to survive losing a member. Only the supervisor Deployment's replica count depends on this value, so changing it neither restarts the gateway nor replaces running members. Ignored when supervisor.autoscaling.enabled is true. | | supervisor.sandboxRuntime.boundaryPort | int | `5500` | Workload boundary TLS listener port. | | tolerations | list | `[]` | Tolerations for the gateway pod. | +| topologySpreadConstraints | list | `[]` | Topology spread constraints for the gateway pod, used to spread replicas across zones or nodes. Empty = omit the field. | | upstreamProxy | object | `{"authAllowInsecure":false,"authSecret":{"key":"","name":""},"caBundle":{"configMapName":"","key":"ca.crt"},"connectByHostname":false,"noProxy":"","url":""}` | Operator-owned corporate forward proxy for policy-approved TLS egress from Kubernetes sandboxes. The workload cannot select or override it. | | upstreamProxy.authAllowInsecure | bool | `false` | Required when authSecret is configured because Basic auth to an HTTP proxy is cleartext. | | upstreamProxy.authSecret.key | string | `""` | Secret key containing the proxy credential. | diff --git a/deploy/helm/openshell/templates/_gateway-workload.tpl b/deploy/helm/openshell/templates/_gateway-workload.tpl index 6b73241e6c..b0814a40b8 100644 --- a/deploy/helm/openshell/templates/_gateway-workload.tpl +++ b/deploy/helm/openshell/templates/_gateway-workload.tpl @@ -358,6 +358,9 @@ spec: {{- end }} - name: gateway-state emptyDir: {} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} @@ -370,4 +373,8 @@ spec: tolerations: {{- toYaml . | nindent 4 }} {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 4 }} + {{- end }} {{- end }} diff --git a/deploy/helm/openshell/tests/gateway_scheduling_test.yaml b/deploy/helm/openshell/tests/gateway_scheduling_test.yaml new file mode 100644 index 0000000000..3a1a755d02 --- /dev/null +++ b/deploy/helm/openshell/tests/gateway_scheduling_test.yaml @@ -0,0 +1,107 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: gateway scheduling fields +templates: + - templates/gateway-config.yaml + - templates/statefulset.yaml + - templates/deployment.yaml +release: + name: openshell + namespace: my-namespace + +tests: + - it: omits scheduling overrides by default on the Deployment + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + asserts: + - notExists: + path: spec.template.spec.priorityClassName + - notExists: + path: spec.template.spec.topologySpreadConstraints + + # priorityClassName — shared pod template, so both workload shapes + - it: omits priorityClassName by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.priorityClassName + + - it: sets priorityClassName on the StatefulSet when configured + template: templates/statefulset.yaml + set: + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + - contains: + path: spec.template.spec.volumes + content: + name: gateway-state + emptyDir: {} + + - it: sets priorityClassName on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + + # topologySpreadConstraints — shared pod template, so both workload shapes + - it: omits topologySpreadConstraints by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.topologySpreadConstraints + + - it: sets topologySpreadConstraints on the StatefulSet when configured + template: templates/statefulset.yaml + set: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].maxSkew + value: 1 + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: topology.kubernetes.io/zone + - equal: + path: spec.template.spec.topologySpreadConstraints[0].whenUnsatisfiable + value: DoNotSchedule + - equal: + path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels + value: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + + - it: sets topologySpreadConstraints on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app.kubernetes.io/name: openshell + app.kubernetes.io/instance: openshell + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: kubernetes.io/hostname diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 0bbb2cd6b2..987744afbb 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -389,6 +389,15 @@ tolerations: [] # -- Affinity rules for the gateway pod. affinity: {} +# -- PriorityClass for the gateway pod. The gateway is a control-plane +# component; when it shares nodes with the workloads it serves, the default +# priority gives it no advantage under node pressure. Empty = omit the field. +priorityClassName: "" + +# -- Topology spread constraints for the gateway pod, used to spread replicas +# across zones or nodes. Empty = omit the field. +topologySpreadConstraints: [] + # -- Non-secret gateway application configuration. Top-level keys name TOML # tables and are rendered into the mounted gateway.toml file. Kubernetes # resource inputs remain outside this map; template expressions derive the diff --git a/docs/kubernetes/setup.mdx b/docs/kubernetes/setup.mdx index fd8d134108..a053a8f709 100644 --- a/docs/kubernetes/setup.mdx +++ b/docs/kubernetes/setup.mdx @@ -300,6 +300,8 @@ The most commonly changed values are: | `global.image.registry` / `global.image.tag` / `global.image.pullPolicy` | Shared registry, tag, and pull policy for the gateway, supervisor, and trusted sandbox runtime. Individual image settings take precedence. | | `gateway.image.registry` / `gateway.image.repository` / `gateway.image.tag` / `gateway.image.digest` | Gateway container image. Defaults to `ghcr.io/nvidia/openshell/gateway:latest`. | | `replicaCount` | Number of gateway replicas. Values above `1` require shared PostgreSQL through `server.externalDbSecret`. | +| `priorityClassName` | Existing Kubernetes PriorityClass for gateway pods. Empty by default. | +| `topologySpreadConstraints` | Kubernetes topology spread constraints for gateway replicas. Empty by default. Set selectors to match the gateway pod labels. | | `workload.kind` | Gateway workload controller. Use `statefulset` for SQLite or `deployment` with `server.externalDbSecret`. | | `workload.allowMultiReplicaStatefulSet` | Allow `replicaCount > 1` with `workload.kind=statefulset`. Prefer Deployment for external database-backed multi-replica gateways. | | `autoscaling.enabled` / `autoscaling.minReplicas` / `autoscaling.maxReplicas` | Render a HorizontalPodAutoscaler instead of a fixed `replicaCount`. Refer to [High Availability](/kubernetes/high-availability#scale-the-gateway). | diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 535a8daf26..3e75574b8e 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -530,6 +530,14 @@ kubectl -n openshell logs -c openshell-gateway --tail=200 `server.telemetryEnabled` renders `OPENSHELL_TELEMETRY_ENABLED` on the gateway pod, and the gateway propagates the effective value to sandbox supervisors. +Gateway pod scheduling accepts top-level `nodeSelector`, `affinity`, +`tolerations`, `priorityClassName`, and `topologySpreadConstraints` values for +both StatefulSet and Deployment workloads. The last two default to unset. +If a gateway pod stays Pending, inspect its scheduling events, confirm the +named PriorityClass exists (`kubectl get priorityclass`), and check that topology +spread selectors match the gateway pod labels and that eligible nodes have the +required topology labels. These values apply only to gateway pods. + #### Additional destination CA ConfigMap When `supervisor.network.additionalCaConfigMapName` is set, Helm must mount the From 53bf840cd22c47c7002d47af7e9f7292b19174a4 Mon Sep 17 00:00:00 2001 From: crm00re Date: Fri, 6 Nov 2026 19:10:03 +0000 Subject: [PATCH 25/31] feat(cli): forward TRACEPARENT/TRACESTATE to gateway for CI trace continuity (#2931) * feat(cli): forward TRACEPARENT/TRACESTATE to gateway for CI trace continuity The CLI's gateway gRPC channel now passively forwards the W3C trace context (TRACEPARENT/TRACESTATE) from its environment, so a CI pipeline's trace parents the gateway's spans. This needs no OTel collector or provider on the CLI side - short-lived clients on CI runners and jump hosts just pass through what the CI system already exports. Add EnvTraceContextInterceptor to openshell-otel: it reads the trace context once at construction and injects it as gRPC metadata, and is a no-op when the context is unset or invalid. Compose it with EdgeAuthInterceptor via a new GatewayInterceptor, since tonic allows only one interceptor per channel. Closes issue #2639 Signed-off-by: Chris Moore * fix(cli): normalize trace-context env reading and cover interceptor composition Review follow-ups on the trace-context forwarding feature: - Normalize each TRACEPARENT/TRACESTATE spelling (trim, drop blanks) before the lower-case fallback so a blank upper-case value no longer shadows a valid lower-case one - Share one env-reading helper between EnvTraceContextInterceptor and set_parent_from_environment so both paths agree on what counts as set - Pass the TRACEPARENT_ENV/TRACESTATE_ENV constants instead of duplicated string literals - Log at debug level when a present TRACEPARENT is invalid, keeping the passive no-op behavior but leaving a grep-able signal - Cover EnvTraceContext::from_env (both spellings, padding, blanks) and the GatewayInterceptor auth+trace composition with tests - Drop tracestate when traceparent is invalid, and assert it in tests - Narrow the traces.mdx claim to standard CLI commands, since term and completions build clients without the trace interceptor Signed-off-by: Chris Moore * fix(otel): address trace-context review findings - gate the OTLP export stack behind the new `export` feature and build the CLI against openshell-otel with default-features = false, so the CLI no longer compiles and links an exporter it never calls - split the env-reading test so the mixed-case steps only run where environment names are case-sensitive; the Windows MSVC lane no longer fails on them - restore mutated env vars through a drop guard so a failed assertion cannot leak them into other tests in the same process - add an SDK test proving auth_interceptor() clones see a refreshed OIDC token through the shared bearer slot - word the trace-forwarding docs around OpenTelemetry integrations instead of implying CI runners export TRACEPARENT natively Signed-off-by: Chris Moore --------- Signed-off-by: Chris Moore --- Cargo.lock | 1 + crates/openshell-cli/Cargo.toml | 1 + crates/openshell-cli/src/tls.rs | 98 ++- crates/openshell-otel/Cargo.toml | 71 +- crates/openshell-otel/src/lib.rs | 840 ++--------------------- crates/openshell-otel/src/propagation.rs | 282 +++++++- crates/openshell-otel/src/setup.rs | 798 +++++++++++++++++++++ crates/openshell-sdk/src/client.rs | 35 + docs/observability/traces.mdx | 2 + 9 files changed, 1298 insertions(+), 830 deletions(-) create mode 100644 crates/openshell-otel/src/setup.rs diff --git a/Cargo.lock b/Cargo.lock index 94cc7b3df6..d178930a4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4388,6 +4388,7 @@ dependencies = [ "openshell-bootstrap", "openshell-core", "openshell-crypto", + "openshell-otel", "openshell-policy", "openshell-providers", "openshell-sdk", diff --git a/crates/openshell-cli/Cargo.toml b/crates/openshell-cli/Cargo.toml index 0cdd32ade0..d7e1c0b8e4 100644 --- a/crates/openshell-cli/Cargo.toml +++ b/crates/openshell-cli/Cargo.toml @@ -18,6 +18,7 @@ path = "src/main.rs" openshell-crypto = { workspace = true } openshell-bootstrap = { path = "../openshell-bootstrap" } openshell-core = { path = "../openshell-core", default-features = false } +openshell-otel = { path = "../openshell-otel", default-features = false } openshell-policy = { path = "../openshell-policy" } openshell-providers = { path = "../openshell-providers" } openshell-sdk = { path = "../openshell-sdk" } diff --git a/crates/openshell-cli/src/tls.rs b/crates/openshell-cli/src/tls.rs index 41381f9a40..fd138aae5b 100644 --- a/crates/openshell-cli/src/tls.rs +++ b/crates/openshell-cli/src/tls.rs @@ -5,6 +5,7 @@ use miette::{IntoDiagnostic, Result, WrapErr}; pub use openshell_core::replica_routing::{ owner_replica, retry_unrouted, routed_to, with_owner_hint, }; +use openshell_otel::EnvTraceContextInterceptor; use openshell_sdk::{AuthConfig, ClientConfig, ClientIdentity, OpenShellClient}; use rustls::{ RootCertStore, @@ -16,7 +17,32 @@ use std::path::PathBuf; use tonic::transport::Channel; /// Concrete gRPC client type used by all commands. -pub type GrpcClient = openshell_sdk::raw::AuthedGrpcClient; +pub type GrpcClient = openshell_sdk::raw::GrpcClient< + tonic::service::interceptor::InterceptedService, +>; + +/// Interceptor stack applied to gateway requests from clients built by +/// [`grpc_client`]. +/// +/// Runs authentication header injection first, then passive W3C trace-context +/// forwarding so a CI pipeline's `TRACEPARENT` extends into the gateway's +/// spans. tonic allows a single interceptor per channel, so the two concerns +/// are composed here. +#[derive(Clone)] +pub struct GatewayInterceptor { + auth: openshell_sdk::EdgeAuthInterceptor, + trace: EnvTraceContextInterceptor, +} + +impl tonic::service::Interceptor for GatewayInterceptor { + fn call( + &mut self, + request: tonic::Request<()>, + ) -> std::result::Result, tonic::Status> { + let request = self.auth.call(request)?; + self.trace.call(request) + } +} #[derive(Clone, Debug, Default)] pub struct TlsOptions { @@ -313,9 +339,10 @@ pub async fn build_channel(server: &str, tls: &TlsOptions) -> Result { /// Build the SDK client and return its raw gRPC escape hatch. /// -/// When `tls.edge_token` is set, the returned client is wrapped with an -/// interceptor that injects authentication headers on every request. -/// Otherwise, standard mTLS is used (interceptor is a no-op). +/// The returned client carries a [`GatewayInterceptor`] that injects +/// authentication headers (when a token is set) and passively forwards the +/// `TRACEPARENT`/`TRACESTATE` trace context from the environment on every +/// request. With no token and no trace context, the interceptor is a no-op. pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result { let config = sdk_client_config(server, tls)?; // Keep the SDK's transport state machine out of every command future. @@ -323,14 +350,71 @@ pub async fn grpc_client(server: &str, tls: &TlsOptions) -> Result { let client = Box::pin(OpenShellClient::connect(config)) .await .into_diagnostic()?; - Ok(client.raw_grpc()) + // tonic allows a single interceptor per channel: compose the SDK's auth + // interceptor (sharing its live bearer slot) with the CLI's passive + // trace-context forwarding. + let interceptor = GatewayInterceptor { + auth: client.auth_interceptor(), + trace: EnvTraceContextInterceptor::from_env(), + }; + Ok(openshell_sdk::raw::GrpcClient::with_interceptor( + client.channel(), + interceptor, + )) } #[cfg(test)] mod tests { - use super::{TlsOptions, sdk_client_config}; - use openshell_sdk::AuthConfig; + use super::{GatewayInterceptor, TlsOptions, sdk_client_config}; + use crate::TEST_ENV_LOCK; + use openshell_otel::EnvTraceContextInterceptor; + use openshell_sdk::{AuthConfig, EdgeAuthInterceptor}; use std::io::Write; + use tonic::service::Interceptor as _; + + #[test] + fn gateway_interceptor_composes_auth_and_trace_forwarding() { + let _guard = TEST_ENV_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + temp_env::with_vars( + [ + ("TRACEPARENT", Some(traceparent)), + ("TRACESTATE", Some("vendor=value")), + ], + || { + let mut interceptor = GatewayInterceptor { + auth: EdgeAuthInterceptor::new(Some("test-token"), None).unwrap(), + trace: EnvTraceContextInterceptor::from_env(), + }; + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert_eq!( + request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()), + Some("Bearer test-token") + ); + assert_eq!( + request + .metadata() + .get("traceparent") + .and_then(|value| value.to_str().ok()), + Some(traceparent) + ); + assert_eq!( + request + .metadata() + .get("tracestate") + .and_then(|value| value.to_str().ok()), + Some("vendor=value") + ); + }, + ); + } #[test] fn sdk_config_carries_cli_mtls_materials() { diff --git a/crates/openshell-otel/Cargo.toml b/crates/openshell-otel/Cargo.toml index c6cd0111c9..a857018b91 100644 --- a/crates/openshell-otel/Cargo.toml +++ b/crates/openshell-otel/Cargo.toml @@ -11,31 +11,31 @@ license.workspace = true repository.workspace = true [dependencies] -async-trait = "0.1" -bytes = { workspace = true } -futures = { workspace = true } +async-trait = { version = "0.1", optional = true } +bytes = { workspace = true, optional = true } +futures = { workspace = true, optional = true } http = { workspace = true } -http-body-util = { workspace = true } -hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "logging", "tls12"] } -hyper-util = { workspace = true, features = ["client-legacy", "http1", "tokio"] } -metrics = { workspace = true } -openshell-crypto = { workspace = true } -openshell-ocsf = { path = "../openshell-ocsf" } +http-body-util = { workspace = true, optional = true } +hyper-rustls = { version = "0.27", default-features = false, optional = true, features = ["http1", "logging", "tls12"] } +hyper-util = { workspace = true, optional = true, features = ["client-legacy", "http1", "tokio"] } +metrics = { workspace = true, optional = true } +openshell-crypto = { workspace = true, optional = true } +openshell-ocsf = { path = "../openshell-ocsf", optional = true } opentelemetry = { workspace = true } -opentelemetry_sdk = { workspace = true, features = ["logs"] } -opentelemetry-http = { workspace = true } -opentelemetry-otlp = { workspace = true, features = ["gzip-http", "http-proto", "logs"] } -rustls = { workspace = true } -rustls-native-certs = { workspace = true } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true } +opentelemetry_sdk = { workspace = true } +opentelemetry-http = { workspace = true, optional = true } +opentelemetry-otlp = { workspace = true, optional = true, features = ["gzip-http", "http-proto", "logs"] } +rustls = { workspace = true, optional = true } +rustls-native-certs = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } +tokio = { workspace = true, optional = true } tracing = { workspace = true } tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true } +tracing-subscriber = { workspace = true, optional = true } tonic = { workspace = true } -tower-http = { workspace = true } -zeroize = { workspace = true } +tower-http = { workspace = true, optional = true } +zeroize = { workspace = true, optional = true } [dev-dependencies] openshell-otel-test-support = { path = "../openshell-otel-test-support" } @@ -44,6 +44,37 @@ opentelemetry_sdk = { workspace = true, features = ["testing"] } prost = { workspace = true } tempfile = "3" tokio = { workspace = true, features = ["test-util"] } +tracing-subscriber = { workspace = true } + +[features] +default = ["export"] +# OTLP/gRPC export: tracer providers, tracing layers, compute-driver and +# gRPC tracing machinery, and the OCSF log bridge. Services that report to +# a collector keep the default; clients that only forward trace context +# (e.g. the CLI) opt out with `default-features = false` so the exporter +# stack stays out of their binaries. +export = [ + "dep:async-trait", + "dep:bytes", + "dep:futures", + "dep:http-body-util", + "dep:hyper-rustls", + "dep:hyper-util", + "dep:metrics", + "dep:openshell-crypto", + "dep:openshell-ocsf", + "dep:opentelemetry-http", + "dep:opentelemetry-otlp", + "dep:rustls", + "dep:rustls-native-certs", + "dep:serde_json", + "dep:thiserror", + "dep:tokio", + "dep:tower-http", + "dep:tracing-subscriber", + "dep:zeroize", + "opentelemetry_sdk/logs", +] [lints] workspace = true diff --git a/crates/openshell-otel/src/lib.rs b/crates/openshell-otel/src/lib.rs index 1f9de6bf17..32077a7f93 100644 --- a/crates/openshell-otel/src/lib.rs +++ b/crates/openshell-otel/src/lib.rs @@ -2,573 +2,79 @@ // SPDX-License-Identifier: Apache-2.0 //! Shared OpenTelemetry trace and log export support for `OpenShell` services. +//! +//! The crate has two tiers: +//! +//! - Trace-context propagation, always compiled in: W3C `traceparent` / +//! `tracestate` extraction and injection for HTTP headers, tonic metadata, +//! and process environments. +//! - The `export` feature (on by default): OTLP tracer providers, tracing +//! layers, compute-driver and gRPC tracing machinery, and the OCSF log +//! bridge. Services that report to a collector keep the default; clients +//! that only forward trace context (e.g. the CLI) opt out with +//! `default-features = false` to keep the exporter stack out of their +//! binaries. -mod correlation; -pub use correlation::current_ocsf_trace_correlation; +mod propagation; + +pub use propagation::{ + EnvTraceContextInterceptor, HeaderMapExtractor, MetadataMapInjector, TRACEPARENT_ENV, + TRACESTATE_ENV, TraceContextInterceptor, current_trace_context_carrier, + current_trace_context_environment, set_parent_from_environment, set_parent_from_trace_context, + trace_context_environment, +}; +#[cfg(feature = "export")] +mod correlation; +#[cfg(feature = "export")] mod driver; +#[cfg(feature = "export")] mod grpc; +#[cfg(feature = "export")] mod headers; +#[cfg(feature = "export")] mod logs; +#[cfg(feature = "export")] mod ocsf; -mod propagation; +#[cfg(feature = "export")] +mod setup; +#[cfg(feature = "export")] mod tls; +#[cfg(feature = "export")] mod transport; #[cfg(test)] #[path = "../../../tests/support/environment.rs"] mod test_environment; +#[cfg(feature = "export")] +pub use correlation::current_ocsf_trace_correlation; + +#[cfg(feature = "export")] pub use driver::{ BoxGrpcStream, ComputeDriverTracing, DriverTracingConfig, DriverTracingHandle, IN_PROCESS_COMPUTE_DRIVER_TARGET, InProcessRpcTracer, install_driver_tracing, }; - +#[cfg(feature = "export")] pub use grpc::{ COMPUTE_DRIVER_RPC_SERVICE, ComputeDriverRpc, ComputeDriverRpcSpan, RecordGrpcFailure, RecordGrpcStatus, TracedGrpcStream, compute_driver_rpc_layer, compute_driver_rpc_operation, grpc_status_code_name, record_grpc_status, rpc, }; +#[cfg(feature = "export")] pub use headers::{encode_otlp_headers, merge_otlp_headers, parse_otlp_headers}; +#[cfg(feature = "export")] pub use ocsf::{OcsfOtlp, OcsfOtlpLayer}; -pub use propagation::{ - HeaderMapExtractor, MetadataMapInjector, TRACEPARENT_ENV, TRACESTATE_ENV, - TraceContextInterceptor, current_trace_context_carrier, current_trace_context_environment, - set_parent_from_environment, set_parent_from_trace_context, trace_context_environment, -}; +#[cfg(feature = "export")] pub use tls::{MAX_TLS_FILE_BYTES, OtlpTls, TlsError, TlsFileError}; - +#[cfg(feature = "export")] pub use transport::{ CONNECT_TIMEOUT, OtlpHttpClient, TCP_KEEPALIVE_IDLE, TCP_KEEPALIVE_INTERVAL, TCP_KEEPALIVE_RETRIES, signal_url, }; -use opentelemetry::KeyValue; -use opentelemetry::trace::TracerProvider as _; -pub use opentelemetry_otlp::LogExporter; -use opentelemetry_otlp::tonic_types::metadata::MetadataMap; -use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig, WithTonicConfig}; -use opentelemetry_sdk::Resource; -use opentelemetry_sdk::trace::SdkTracer; -pub use opentelemetry_sdk::trace::SdkTracerProvider; -use tracing::Subscriber; -use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::Layer as _; -use tracing_subscriber::layer::{Context, Filter}; -use tracing_subscriber::registry::LookupSpan; - -/// Export supervisor spans at INFO by default, or DEBUG and TRACE when enabled. -/// Dependency spans remain at INFO. -#[must_use] -pub fn supervisor_span_filter(log_level: &str) -> tracing_subscriber::EnvFilter { - use tracing_subscriber::filter::LevelFilter; - let level = log_level - .parse::() - .unwrap_or(LevelFilter::INFO) - .max(LevelFilter::INFO); - tracing_subscriber::EnvFilter::new(format!("info,openshell={level}")) -} - -const SDK_UNKNOWN_SERVICE_PREFIX: &str = "unknown_service"; - -/// Build the resource attributes shared by gateway and compute-driver providers. -pub fn gateway_resource_attributes( - gateway_name: Option<&str>, - compute_driver: Option<&str>, -) -> Vec { - let mut attributes = Vec::new(); - if let Some(name) = gateway_name.map(str::trim).filter(|name| !name.is_empty()) { - attributes.push(KeyValue::new("openshell.gateway.name", name.to_string())); - } - if let Some(driver) = compute_driver - .map(str::trim) - .filter(|driver| !driver.is_empty()) - { - attributes.push(KeyValue::new( - "openshell.gateway.compute_driver", - driver.to_string(), - )); - } - attributes -} - -/// Mark `span` as failed. -/// -/// The field must be declared on the span at creation because `tracing` drops -/// records for fields a span does not have. -pub fn mark_error(span: &tracing::Span) { - span.record("otel.status_code", "ERROR"); -} - -/// Marks the current span when an instrumented operation returns an error. -pub fn record_error_result(result: Result) -> Result { - if result.is_err() { - mark_error(&tracing::Span::current()); - } - result -} - -/// Marks an instrumented function's span when it exits before returning success. -/// -/// Create the guard at the start of the instrumented function and return the -/// successful result through [`Self::finish`]. An early `?` or error return -/// drops the unfinished guard and marks the captured span as failed. -#[must_use] -pub struct ErrorStatusGuard { - span: tracing::Span, - finished: bool, -} - -impl ErrorStatusGuard { - /// Captures the current instrumented span. - pub fn current() -> Self { - Self { - span: tracing::Span::current(), - finished: false, - } - } - - /// Returns `result`, marking this guard complete when it is successful. - pub fn finish(mut self, result: Result) -> Result { - self.finished = result.is_ok(); - result - } -} - -impl Drop for ErrorStatusGuard { - fn drop(&mut self) { - if !self.finished { - mark_error(&self.span); - } - } -} - -/// How a process chooses its OpenTelemetry `service.name`. -#[derive(Debug, Clone, Copy)] -pub enum ServiceName<'a> { - /// Always use this name, overriding `OTEL_SERVICE_NAME`. - Fixed(&'a str), - /// Use `OTEL_SERVICE_NAME` when set, otherwise use this default. - EnvironmentOr(&'a str), -} - -/// OTLP transport to a collector. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub enum Protocol { - #[default] - Grpc, - HttpProtobuf, -} - -/// Inputs for OTLP export. -#[derive(Debug, Clone)] -pub struct OtlpExportConfig<'a> { - pub endpoint: &'a str, - pub service_name: ServiceName<'a>, - pub service_version: Option<&'a str>, - pub resource_attributes: Vec, -} - -/// How exports reach a collector: the transport, request headers, and TLS. -#[derive(Clone, Default, PartialEq, Eq)] -pub struct CollectorTransport { - pub protocol: Protocol, - pub headers: http::HeaderMap, - pub tls: OtlpTls, -} - -impl std::fmt::Debug for CollectorTransport { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("CollectorTransport") - .field("protocol", &self.protocol) - .field("header_names", &self.headers.keys().collect::>()) - .field("tls", &self.tls) - .finish() - } -} - -/// Failure to set up OTLP export. -#[derive(Debug, thiserror::Error)] -pub enum SetupError { - #[error("OTLP endpoint is empty")] - EmptyEndpoint, - - #[error("invalid OTLP endpoint {endpoint:?}: {source}")] - InvalidEndpoint { - endpoint: String, - source: http::uri::InvalidUri, - }, - - #[error("failed to build the OTLP exporter: {0}")] - Exporter(#[from] opentelemetry_otlp::ExporterBuildError), - - #[error("invalid OTLP endpoint {endpoint:?}: {source}")] - InvalidSignalUrl { - endpoint: String, - source: http::Error, - }, - - #[error("OTLP TLS settings need an https:// endpoint, not {endpoint:?}")] - TlsWithoutHttps { endpoint: String }, - - #[error("invalid OTLP TLS settings: {0}")] - Tls(#[from] TlsError), - - #[error("invalid OTLP TLS files: {0}")] - TlsFiles(#[from] TlsFileError), - - #[error("OTLP/HTTP export needs a tokio runtime")] - NoRuntime, -} - -fn resource_attributes(config: &OtlpExportConfig<'_>) -> Vec { - let mut attributes = config.resource_attributes.clone(); - if let Some(version) = config - .service_version - .map(str::trim) - .filter(|version| !version.is_empty()) - { - attributes.push(KeyValue::new("service.version", version.to_string())); - } - attributes -} - -/// Build the OpenTelemetry resource for an export configuration. -#[must_use] -pub fn resource_for(config: &OtlpExportConfig<'_>) -> Resource { - let attributes = resource_attributes(config); - match config.service_name { - ServiceName::Fixed(name) => Resource::builder() - .with_service_name(name.trim().to_string()) - .with_attributes(attributes) - .build(), - ServiceName::EnvironmentOr(default) => { - let detected = Resource::builder() - .with_attributes(attributes.clone()) - .build(); - if detected - .get(&opentelemetry::Key::from_static_str("service.name")) - .is_some_and(|value| !value.to_string().starts_with(SDK_UNKNOWN_SERVICE_PREFIX)) - { - detected - } else { - Resource::builder() - .with_service_name(default.trim().to_string()) - .with_attributes(attributes) - .build() - } - } - } -} - -/// Trim and validate an OTLP endpoint before handing it to an exporter. -/// -/// Every exporter that reads an operator-supplied endpoint uses this, so a -/// value accepted for one signal is accepted for all of them. -pub fn validated_endpoint(endpoint: &str) -> Result<(&str, http::Uri), SetupError> { - let endpoint = endpoint.trim(); - if endpoint.is_empty() { - return Err(SetupError::EmptyEndpoint); - } - let uri = endpoint - .parse::() - .map_err(|source| SetupError::InvalidEndpoint { - endpoint: endpoint.to_string(), - source, - })?; - Ok((endpoint, uri)) -} - -/// A validated OTLP collector endpoint and the resource that every signal -/// exported to it describes. -#[derive(Clone)] -pub struct OtlpTarget { - endpoint: String, - tls: bool, - protocol: Protocol, - headers: http::HeaderMap, - tls_settings: OtlpTls, - resource: Resource, -} - -impl std::fmt::Debug for OtlpTarget { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("OtlpTarget") - .field("endpoint", &self.endpoint) - .field("tls", &self.tls) - .field("protocol", &self.protocol) - .field("header_names", &self.headers.keys().collect::>()) - .field("tls_settings", &self.tls_settings) - .field("resource", &self.resource) - .finish() - } -} - -impl OtlpTarget { - /// Validate the endpoint and build the resource. - pub fn new(config: &OtlpExportConfig<'_>) -> Result { - let (endpoint, uri) = validated_endpoint(config.endpoint)?; - Ok(Self { - endpoint: endpoint.to_string(), - tls: uri.scheme() == Some(&http::uri::Scheme::HTTPS), - protocol: Protocol::Grpc, - headers: http::HeaderMap::new(), - tls_settings: OtlpTls::default(), - resource: resource_for(config), - }) - } - - /// Export over `protocol` instead of gRPC. - #[must_use] - pub fn with_protocol(mut self, protocol: Protocol) -> Self { - self.protocol = protocol; - self - } - - /// Send `headers` with every export request. Headers from the process - /// environment (`OTEL_EXPORTER_OTLP_*HEADERS`) still apply and replace - /// any of the same name. - #[must_use] - pub fn with_headers(mut self, headers: http::HeaderMap) -> Self { - self.headers = headers; - self - } - - /// Reach the collector the way `transport` describes. - #[must_use] - pub fn with_transport(self, transport: &CollectorTransport) -> Self { - self.with_protocol(transport.protocol) - .with_headers(transport.headers.clone()) - .with_tls(transport.tls.clone()) - } - - /// Verify an `https://` collector and authenticate to it with `tls`. - #[must_use] - pub fn with_tls(mut self, tls: OtlpTls) -> Self { - self.tls_settings = tls; - self - } - - /// The URL of `signal_path` under the endpoint, for OTLP/HTTP export. - pub(crate) fn http_signal_url(&self, signal_path: &str) -> Result { - signal_url(&self.endpoint, signal_path) - .map(|url| url.to_string()) - .map_err(|source| SetupError::InvalidSignalUrl { - endpoint: self.endpoint.clone(), - source, - }) - } - - /// Point an exporter builder at this target, over TLS for `https://`. - pub(crate) fn configure(&self, builder: B) -> B - where - B: WithExportConfig + WithTonicConfig, - { - let builder = builder - .with_endpoint(self.endpoint.clone()) - .with_metadata(MetadataMap::from_headers(self.headers.clone())); - if self.tls { - builder.with_tls_config(self.tls_settings.grpc_config()) - } else { - builder - } - } - - /// Point an OTLP/HTTP exporter builder at `signal_path` under this - /// target. - pub(crate) fn configure_http( - &self, - builder: B, - signal_path: &str, - timeout_var: &str, - ) -> Result - where - B: WithExportConfig + WithHttpConfig, - { - // The SDK percent-decodes these values again. - let headers = self - .headers - .iter() - .filter_map(|(name, value)| { - Some((name.to_string(), value.to_str().ok()?.replace('%', "%25"))) - }) - .collect(); - Ok(builder - .with_protocol(opentelemetry_otlp::Protocol::HttpBinary) - .with_endpoint(self.http_signal_url(signal_path)?) - .with_http_client(OtlpHttpClient::new( - self.tls.then_some(&self.tls_settings), - transport::export_timeout(timeout_var), - )?) - .with_headers(headers)) - } - - #[must_use] - pub fn endpoint(&self) -> &str { - &self.endpoint - } - - #[must_use] - pub fn resource(&self) -> &Resource { - &self.resource - } - - /// Fails when TLS settings are given for a plaintext endpoint. - pub(crate) fn check_tls(&self) -> Result<(), SetupError> { - if self.tls || self.tls_settings.is_default() { - Ok(()) - } else { - Err(SetupError::TlsWithoutHttps { - endpoint: self.endpoint.clone(), - }) - } - } - - /// Build a trace provider that exports spans to this target in batches. - pub fn tracer_provider(&self) -> Result { - self.check_tls()?; - let exporter = match self.protocol { - Protocol::Grpc => self - .configure(SpanExporter::builder().with_tonic()) - .build()?, - Protocol::HttpProtobuf => self - .configure_http( - SpanExporter::builder().with_http(), - "v1/traces", - "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", - )? - .build()?, - }; - Ok(SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource(self.resource.clone()) - .build()) - } -} - -/// Build an OTLP trace provider. -pub fn build_provider(config: &OtlpExportConfig<'_>) -> Result { - OtlpTarget::new(config)?.tracer_provider() -} - -/// Build the provider for an optional OTLP configuration. -/// -/// Telemetry setup failures disable export and remain available for the caller -/// to report after its tracing subscriber is installed. -#[must_use] -pub fn provider_for( - config: Option>, -) -> (Option, Option) { - match config.as_ref().map(build_provider) { - None => (None, None), - Some(Ok(provider)) => (Some(provider), None), - Some(Err(error)) => (None, Some(error)), - } -} - -/// Filtered OpenTelemetry layer returned by [`layer`]. -pub type OtlpLayer = tracing_subscriber::filter::Filtered< - OpenTelemetryLayer, - tracing_subscriber::filter::FilterFn, - S, ->; - -pub type TargetOtlpLayer = - tracing_subscriber::filter::Filtered, TargetPrefixFilter, S>; - -#[derive(Debug, Clone)] -pub struct TargetPrefixFilter { - prefixes: Vec<&'static str>, - include: bool, -} - -impl Filter for TargetPrefixFilter { - fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: &Context<'_, S>) -> bool { - let matches_prefix = self - .prefixes - .iter() - .any(|prefix| metadata.target().starts_with(prefix)); - metadata.is_span() - && !metadata.target().starts_with("opentelemetry") - && (matches_prefix == self.include) - } -} - -/// Build a tracing layer that exports spans and excludes exporter callsites. -pub fn layer(provider: &SdkTracerProvider, instrumentation_scope: &'static str) -> OtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { - metadata.is_span() && !metadata.target().starts_with("opentelemetry") - })) -} - -/// Build a tracing layer that exports only spans from `target_prefix`. -pub fn layer_for_target_prefix( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefix: &'static str, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - layer_for_target_prefixes(provider, instrumentation_scope, [target_prefix]) -} - -/// Build a tracing layer that exports only spans from `target_prefixes`. -pub fn layer_for_target_prefixes( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefixes: impl IntoIterator, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(TargetPrefixFilter { - prefixes: target_prefixes.into_iter().collect(), - include: true, - }) -} - -/// Build a tracing layer that excludes spans from `target_prefix`. -/// -/// `None` excludes no application spans. -pub fn layer_excluding_target_prefix( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefix: Option<&'static str>, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - layer_excluding_target_prefixes(provider, instrumentation_scope, target_prefix) -} - -/// Build a tracing layer that excludes spans from `target_prefixes`. -/// -/// An empty iterator excludes no application spans. -pub fn layer_excluding_target_prefixes( - provider: &SdkTracerProvider, - instrumentation_scope: &'static str, - target_prefixes: impl IntoIterator, -) -> TargetOtlpLayer -where - S: Subscriber + for<'span> LookupSpan<'span>, -{ - tracing_opentelemetry::layer() - .with_tracer(provider.tracer(instrumentation_scope)) - .with_filter(TargetPrefixFilter { - prefixes: target_prefixes.into_iter().collect(), - include: false, - }) -} +#[cfg(feature = "export")] +pub use setup::*; #[cfg(test)] pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { @@ -576,265 +82,3 @@ pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> { LOCK.lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn supervisor_span_filter_raises_only_openshell_targets() { - let directives = |log_level: &str| supervisor_span_filter(log_level).to_string(); - assert_eq!(directives("warn"), "openshell=info,info"); - assert_eq!(directives("info"), "openshell=info,info"); - assert_eq!(directives("debug"), "openshell=debug,info"); - assert_eq!(directives("info,h2=debug"), "openshell=info,info"); - } - - #[test] - fn error_status_guard_marks_only_unfinished_results() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let provider = SdkTracerProvider::builder() - .with_simple_exporter(exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry().with(layer(&provider, "guard-test")); - - tracing::subscriber::with_default(subscriber, || { - let failed = - tracing::info_span!("failed-operation", otel.status_code = tracing::field::Empty); - { - let _entered = failed.enter(); - drop(ErrorStatusGuard::current()); - } - drop(failed); - - let succeeded = tracing::info_span!( - "successful-operation", - otel.status_code = tracing::field::Empty - ); - { - let _entered = succeeded.enter(); - ErrorStatusGuard::current().finish(Ok::<_, ()>(())).unwrap(); - } - drop(succeeded); - }); - - let spans = exporter.get_finished_spans().unwrap(); - let failed = spans - .iter() - .find(|span| span.name == "failed-operation") - .unwrap(); - assert!(matches!( - failed.status, - opentelemetry::trace::Status::Error { .. } - )); - let succeeded = spans - .iter() - .find(|span| span.name == "successful-operation") - .unwrap(); - assert_eq!(succeeded.status, opentelemetry::trace::Status::Unset); - } - - #[test] - fn target_scoped_layers_partition_in_process_driver_spans() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let gateway_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let gateway_provider = SdkTracerProvider::builder() - .with_simple_exporter(gateway_exporter.clone()) - .build(); - let driver_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let driver_provider = SdkTracerProvider::builder() - .with_simple_exporter(driver_exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry() - .with(layer_excluding_target_prefix( - &gateway_provider, - "gateway", - Some("driver_target"), - )) - .with(layer_for_target_prefix( - &driver_provider, - "driver", - "driver_target", - )); - - tracing::subscriber::with_default(subscriber, || { - let gateway = tracing::info_span!(target: "gateway_target", "gateway.span"); - let _entered = gateway.enter(); - drop(tracing::info_span!(target: "driver_target::operation", "driver.span")); - }); - - gateway_provider.force_flush().unwrap(); - driver_provider.force_flush().unwrap(); - let gateway_spans = gateway_exporter.get_finished_spans().unwrap(); - let driver_spans = driver_exporter.get_finished_spans().unwrap(); - assert_eq!( - gateway_spans - .iter() - .map(|span| span.name.as_ref()) - .collect::>(), - ["gateway.span"] - ); - assert_eq!( - driver_spans - .iter() - .map(|span| span.name.as_ref()) - .collect::>(), - ["driver.span"] - ); - assert_eq!( - driver_spans[0].span_context.trace_id(), - gateway_spans[0].span_context.trace_id() - ); - assert_eq!( - driver_spans[0].parent_span_id, - gateway_spans[0].span_context.span_id() - ); - } - - #[test] - fn excluding_no_target_prefix_exports_all_application_spans() { - use tracing_subscriber::layer::SubscriberExt as _; - - let _tracing_lock = test_lock(); - let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); - let provider = SdkTracerProvider::builder() - .with_simple_exporter(exporter.clone()) - .build(); - let subscriber = tracing_subscriber::registry() - .with(layer_excluding_target_prefix(&provider, "gateway", None)); - - tracing::subscriber::with_default(subscriber, || { - drop(tracing::info_span!(target: "\0driver_target", "nul-prefixed.span")); - drop(tracing::info_span!(target: "gateway_target", "gateway.span")); - }); - - provider.force_flush().unwrap(); - let spans = exporter.get_finished_spans().unwrap(); - assert_eq!(spans.len(), 2); - assert!(spans.iter().any(|span| span.name == "nul-prefixed.span")); - assert!(spans.iter().any(|span| span.name == "gateway.span")); - } - - #[test] - fn resource_uses_fixed_service_identity_and_custom_attributes() { - let resource = resource_for(&OtlpExportConfig { - endpoint: "http://127.0.0.1:4317", - service_name: ServiceName::Fixed("openshell-driver-vm"), - service_version: Some("1.2.3"), - resource_attributes: vec![KeyValue::new("openshell.gateway.name", "vm-dev")], - }); - - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str("service.name")) - .map(|value| value.to_string()), - Some("openshell-driver-vm".to_string()) - ); - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str("service.version")) - .map(|value| value.to_string()), - Some("1.2.3".to_string()) - ); - assert_eq!( - resource - .get(&opentelemetry::Key::from_static_str( - "openshell.gateway.name", - )) - .map(|value| value.to_string()), - Some("vm-dev".to_string()) - ); - } - - #[tokio::test] - async fn tls_settings_need_an_https_endpoint() { - let pki = openshell_otel_test_support::TestPki::generate(); - let tls = OtlpTls::from_pem(Some(pki.ca_pem.as_bytes()), None).unwrap(); - for protocol in [Protocol::Grpc, Protocol::HttpProtobuf] { - let target = |endpoint| { - OtlpTarget::new(&OtlpExportConfig { - endpoint, - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_protocol(protocol) - .with_tls(tls.clone()) - }; - assert!(matches!( - target("http://collector:4317").tracer_provider(), - Err(SetupError::TlsWithoutHttps { .. }) - )); - assert!(matches!( - target("http://collector:4317").log_exporter(), - Err(SetupError::TlsWithoutHttps { .. }) - )); - assert!(target("https://collector:4317").tracer_provider().is_ok()); - } - } - - #[test] - fn debug_output_names_headers_without_their_values() { - let target = OtlpTarget::new(&OtlpExportConfig { - endpoint: "http://collector:4317", - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_headers(parse_otlp_headers("authorization=Bearer%20secret-token")); - - let debug = format!("{target:?}"); - assert!(debug.contains("authorization"), "{debug}"); - assert!(!debug.contains("secret-token"), "{debug}"); - } - - #[tokio::test(flavor = "multi_thread")] - async fn trace_provider_sends_headers_as_grpc_metadata() { - test_environment::Environment::new() - .remove("OTEL_EXPORTER_OTLP_HEADERS") - .remove("OTEL_EXPORTER_OTLP_TRACES_HEADERS") - .remove("OTEL_EXPORTER_OTLP_LOGS_HEADERS") - .run_async(async { - use opentelemetry::trace::Tracer as _; - - let collector = openshell_otel_test_support::OtlpTestServer::start().await; - let provider = OtlpTarget::new(&OtlpExportConfig { - endpoint: collector.endpoint(), - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - }) - .unwrap() - .with_headers(parse_otlp_headers("authorization=Bearer%20token")) - .tracer_provider() - .unwrap(); - provider.tracer("test").in_span("grpc-span", |_| {}); - let flush = tokio::task::spawn_blocking(move || provider.force_flush()); - - collector.wait_for_export().await; - flush.await.unwrap().expect("flush succeeds"); - assert_eq!(collector.received().trace_authorizations, ["Bearer token"]); - }) - .await; - } - - #[tokio::test] - async fn malformed_endpoint_disables_export_with_a_reportable_error() { - let (provider, error) = provider_for(Some(OtlpExportConfig { - endpoint: "definitely not a url", - service_name: ServiceName::Fixed("test-service"), - service_version: None, - resource_attributes: Vec::new(), - })); - - assert!(provider.is_none()); - assert!(matches!(error, Some(SetupError::InvalidEndpoint { .. }))); - } -} diff --git a/crates/openshell-otel/src/propagation.rs b/crates/openshell-otel/src/propagation.rs index 02f2bdb120..5bb7ba5ecc 100644 --- a/crates/openshell-otel/src/propagation.rs +++ b/crates/openshell-otel/src/propagation.rs @@ -6,6 +6,7 @@ use std::collections::BTreeMap; use http::HeaderMap; +use opentelemetry::Context; use opentelemetry::propagation::{Extractor, Injector, TextMapPropagator}; use opentelemetry_sdk::propagation::TraceContextPropagator; use tracing_opentelemetry::OpenTelemetrySpanExt as _; @@ -112,12 +113,16 @@ pub fn current_trace_context_environment() -> Vec<(&'static str, String)> { /// Parent `span` under the trace context in `TRACEPARENT` and `TRACESTATE`. /// +/// Reads the environment the same way [`EnvTraceContextInterceptor`] does: +/// both spellings are accepted, whitespace is trimmed, and blank values are +/// treated as unset. +/// /// Leaves `span` unchanged when the environment carries no valid context. pub fn set_parent_from_environment(span: &tracing::Span) { set_parent_from_trace_context( span, - std::env::var(TRACEPARENT_ENV).ok().as_deref(), - std::env::var(TRACESTATE_ENV).ok().as_deref(), + read_env(TRACEPARENT_ENV).as_deref(), + read_env(TRACESTATE_ENV).as_deref(), ); } @@ -177,22 +182,142 @@ impl tonic::service::Interceptor for TraceContextInterceptor { } } +/// Reads W3C trace-context propagation fields from process environment +/// variables (`TRACEPARENT` and `TRACESTATE`). +/// +/// OpenTelemetry integrations in CI systems (for example the Jenkins +/// OpenTelemetry plugin or an OpenTelemetry GitHub Action) export these +/// variables to propagate the pipeline trace to child processes. +/// Both upper-case and lower-case spellings are accepted; blank values are +/// treated as unset. +#[derive(Debug, Clone, Default)] +struct EnvTraceContext { + traceparent: Option, + tracestate: Option, +} + +impl EnvTraceContext { + fn from_env() -> Self { + Self { + traceparent: read_env(TRACEPARENT_ENV), + tracestate: read_env(TRACESTATE_ENV), + } + } +} + +/// Read one environment variable, trimming whitespace and discarding blanks. +fn read_one(name: &str) -> Option { + std::env::var(name) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +/// Read an environment variable by its upper-case name, falling back to the +/// lower-case spelling, trimming whitespace and discarding blank values. +/// +/// Each spelling is normalized before the fallback, so a blank upper-case +/// value never shadows a valid lower-case one. +fn read_env(upper: &str) -> Option { + read_one(upper).or_else(|| read_one(&upper.to_ascii_lowercase())) +} + +impl Extractor for EnvTraceContext { + fn get(&self, key: &str) -> Option<&str> { + match key { + "traceparent" => self.traceparent.as_deref(), + "tracestate" => self.tracestate.as_deref(), + _ => None, + } + } + + fn keys(&self) -> Vec<&str> { + let mut keys = Vec::new(); + if self.traceparent.is_some() { + keys.push("traceparent"); + } + if self.tracestate.is_some() { + keys.push("tracestate"); + } + keys + } +} + +/// Passively forwards a W3C trace context captured from the process +/// environment onto outbound tonic requests. +/// +/// Unlike [`TraceContextInterceptor`], this does not require an active +/// OpenTelemetry span or an `SdkTracerProvider`; it reads `TRACEPARENT` +/// (and optionally `TRACESTATE`) once at construction and injects them as +/// gRPC metadata. This lets short-lived clients (e.g. the CLI on a CI runner) +/// extend a pipeline trace to the gateway without any collector configuration. +/// +/// When `TRACEPARENT` is unset or invalid the captured context carries no +/// valid span, so injection is a no-op. +#[derive(Debug, Clone)] +pub struct EnvTraceContextInterceptor { + context: Context, +} + +impl EnvTraceContextInterceptor { + /// Capture the W3C trace context from the process environment. + #[must_use] + pub fn from_env() -> Self { + let env = EnvTraceContext::from_env(); + let context = context_from(&env); + if env.traceparent.is_some() && !span_context_is_valid(&context) { + tracing::debug!("TRACEPARENT is set but invalid; not forwarding trace context"); + } + Self { context } + } +} + +/// Extract a [`Context`] from an [`Extractor`] using a fresh base context, so +/// the result depends only on the extractor and not on any ambient context. +fn context_from(extractor: &impl Extractor) -> Context { + TraceContextPropagator::new().extract_with_context(&Context::new(), extractor) +} + +fn span_context_is_valid(context: &Context) -> bool { + use opentelemetry::trace::TraceContextExt as _; + + context.span().span_context().is_valid() +} + +impl tonic::service::Interceptor for EnvTraceContextInterceptor { + fn call( + &mut self, + mut request: tonic::Request<()>, + ) -> Result, tonic::Status> { + TraceContextPropagator::new().inject_context( + &self.context, + &mut MetadataMapInjector::new(request.metadata_mut()), + ); + Ok(request) + } +} + #[cfg(test)] mod tests { use super::*; + use tonic::service::Interceptor as _; fn env_test_exporter() -> ( opentelemetry_sdk::trace::SdkTracerProvider, opentelemetry_sdk::trace::InMemorySpanExporter, impl tracing::Subscriber + Send + Sync, ) { + use opentelemetry::trace::TracerProvider as _; use tracing_subscriber::layer::SubscriberExt as _; let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "env-test")); + // Built directly from `tracing_opentelemetry` so the test runs even + // without the `export` feature. + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("env-test"))); (provider, exporter, subscriber) } @@ -263,6 +388,7 @@ mod tests { #[test] fn invalid_environment_carrier_leaves_the_span_unparented() { + use opentelemetry::trace::TracerProvider as _; use tracing_subscriber::layer::SubscriberExt as _; let _tracing_lock = crate::test_lock(); @@ -270,7 +396,8 @@ mod tests { let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "env-test")); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("env-test"))); tracing::subscriber::with_default(subscriber, || { let span = tracing::info_span!("orphan"); @@ -285,12 +412,14 @@ mod tests { #[test] fn interceptor_adds_traceparent_only_inside_an_exported_span() { + use opentelemetry::trace::TracerProvider as _; use tonic::service::Interceptor as _; use tracing_subscriber::layer::SubscriberExt as _; let _tracing_lock = crate::test_lock(); let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder().build(); - let subscriber = tracing_subscriber::registry().with(crate::layer(&provider, "rpc-test")); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("rpc-test"))); let (outside, inside) = tracing::subscriber::with_default(subscriber, || { let outside = TraceContextInterceptor @@ -330,4 +459,147 @@ mod tests { Some("value") ); } + + fn interceptor_with(extractor: EnvTraceContext) -> EnvTraceContextInterceptor { + EnvTraceContextInterceptor { + context: context_from(&extractor), + } + } + + /// Restores snapshotted environment variables when dropped, so a failed + /// assertion cannot leak them into later tests sharing the process. + struct EnvRestore(Vec<(&'static str, Option)>); + + impl EnvRestore { + fn snapshot(names: &[&'static str]) -> Self { + Self( + names + .iter() + .map(|name| (*name, std::env::var(name).ok())) + .collect(), + ) + } + } + + impl Drop for EnvRestore { + #[allow(unsafe_code)] + fn drop(&mut self) { + for (name, value) in &self.0 { + unsafe { + match value { + Some(value) => std::env::set_var(name, value), + None => std::env::remove_var(name), + } + } + } + } + } + + #[test] + #[allow(unsafe_code)] + fn env_trace_context_from_env_trims_and_skips_blanks() { + let _tracing_lock = crate::test_lock(); + let _restore = EnvRestore::snapshot(&[TRACEPARENT_ENV, TRACESTATE_ENV]); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + + // Upper-case spelling with surrounding whitespace. + unsafe { + std::env::set_var(TRACEPARENT_ENV, format!(" {traceparent} ")); + std::env::set_var(TRACESTATE_ENV, " vendor=value "); + } + let padded = EnvTraceContext::from_env(); + assert_eq!(padded.traceparent.as_deref(), Some(traceparent)); + assert_eq!(padded.tracestate.as_deref(), Some("vendor=value")); + + // Blank values are treated as unset. + unsafe { + std::env::set_var(TRACEPARENT_ENV, " "); + std::env::set_var(TRACESTATE_ENV, ""); + } + let blank = EnvTraceContext::from_env(); + assert_eq!(blank.traceparent, None); + assert_eq!(blank.tracestate, None); + } + + /// Windows treats environment names case-insensitively, so the lower-case + /// fallback cannot be exercised there: `TRACEPARENT` and `traceparent` + /// are the same variable. + #[test] + #[cfg(not(windows))] + #[allow(unsafe_code)] + fn env_trace_context_from_env_reads_lower_case_spelling() { + let _tracing_lock = crate::test_lock(); + let _restore = + EnvRestore::snapshot(&[TRACEPARENT_ENV, "traceparent", TRACESTATE_ENV, "tracestate"]); + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + + // Lower-case-only spelling. + unsafe { + std::env::remove_var(TRACEPARENT_ENV); + std::env::remove_var(TRACESTATE_ENV); + std::env::set_var("traceparent", traceparent); + std::env::set_var("tracestate", "vendor=value"); + } + let lower = EnvTraceContext::from_env(); + assert_eq!(lower.traceparent.as_deref(), Some(traceparent)); + assert_eq!(lower.tracestate.as_deref(), Some("vendor=value")); + + // A blank upper-case value must not shadow a valid lower-case one. + unsafe { + std::env::set_var(TRACEPARENT_ENV, " "); + std::env::remove_var("tracestate"); + } + let shadowed = EnvTraceContext::from_env(); + assert_eq!(shadowed.traceparent.as_deref(), Some(traceparent)); + assert_eq!(shadowed.tracestate, None); + } + + #[test] + fn env_interceptor_forwards_valid_traceparent() { + let traceparent = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + let mut interceptor = interceptor_with(EnvTraceContext { + traceparent: Some(traceparent.to_string()), + tracestate: Some("vendor=value".to_string()), + }); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert_eq!( + request + .metadata() + .get("traceparent") + .and_then(|value| value.to_str().ok()), + Some(traceparent) + ); + assert_eq!( + request + .metadata() + .get("tracestate") + .and_then(|value| value.to_str().ok()), + Some("vendor=value") + ); + } + + #[test] + fn env_interceptor_is_noop_without_traceparent() { + let mut interceptor = interceptor_with(EnvTraceContext::default()); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert!(request.metadata().get("traceparent").is_none()); + assert!(request.metadata().get("tracestate").is_none()); + } + + #[test] + fn env_interceptor_is_noop_for_invalid_traceparent() { + let mut interceptor = interceptor_with(EnvTraceContext { + traceparent: Some("not-a-valid-traceparent".to_string()), + tracestate: Some("vendor=value".to_string()), + }); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + + assert!(request.metadata().get("traceparent").is_none()); + assert!(request.metadata().get("tracestate").is_none()); + } } diff --git a/crates/openshell-otel/src/setup.rs b/crates/openshell-otel/src/setup.rs new file mode 100644 index 0000000000..1bf8407e59 --- /dev/null +++ b/crates/openshell-otel/src/setup.rs @@ -0,0 +1,798 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OTLP export setup: resources, providers, and tracing layers. +//! +//! Compiled only with the `export` feature; see the crate-level documentation. + +use crate::tls::{OtlpTls, TlsError, TlsFileError}; +use crate::transport::{self, OtlpHttpClient, signal_url}; +use opentelemetry::KeyValue; +use opentelemetry::trace::TracerProvider as _; +pub use opentelemetry_otlp::LogExporter; +use opentelemetry_otlp::tonic_types::metadata::MetadataMap; +use opentelemetry_otlp::{SpanExporter, WithExportConfig, WithHttpConfig, WithTonicConfig}; +use opentelemetry_sdk::Resource; +use opentelemetry_sdk::trace::SdkTracer; +pub use opentelemetry_sdk::trace::SdkTracerProvider; +use tracing::Subscriber; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::Layer as _; +use tracing_subscriber::layer::{Context, Filter}; +use tracing_subscriber::registry::LookupSpan; + +/// Export supervisor spans at INFO by default, or DEBUG and TRACE when enabled. +/// Dependency spans remain at INFO. +#[must_use] +pub fn supervisor_span_filter(log_level: &str) -> tracing_subscriber::EnvFilter { + use tracing_subscriber::filter::LevelFilter; + let level = log_level + .parse::() + .unwrap_or(LevelFilter::INFO) + .max(LevelFilter::INFO); + tracing_subscriber::EnvFilter::new(format!("info,openshell={level}")) +} + +const SDK_UNKNOWN_SERVICE_PREFIX: &str = "unknown_service"; + +/// Build the resource attributes shared by gateway and compute-driver providers. +pub fn gateway_resource_attributes( + gateway_name: Option<&str>, + compute_driver: Option<&str>, +) -> Vec { + let mut attributes = Vec::new(); + if let Some(name) = gateway_name.map(str::trim).filter(|name| !name.is_empty()) { + attributes.push(KeyValue::new("openshell.gateway.name", name.to_string())); + } + if let Some(driver) = compute_driver + .map(str::trim) + .filter(|driver| !driver.is_empty()) + { + attributes.push(KeyValue::new( + "openshell.gateway.compute_driver", + driver.to_string(), + )); + } + attributes +} + +/// Mark `span` as failed. +/// +/// The field must be declared on the span at creation because `tracing` drops +/// records for fields a span does not have. +pub fn mark_error(span: &tracing::Span) { + span.record("otel.status_code", "ERROR"); +} + +/// Marks the current span when an instrumented operation returns an error. +pub fn record_error_result(result: Result) -> Result { + if result.is_err() { + mark_error(&tracing::Span::current()); + } + result +} + +/// Marks an instrumented function's span when it exits before returning success. +/// +/// Create the guard at the start of the instrumented function and return the +/// successful result through [`Self::finish`]. An early `?` or error return +/// drops the unfinished guard and marks the captured span as failed. +#[must_use] +pub struct ErrorStatusGuard { + span: tracing::Span, + finished: bool, +} + +impl ErrorStatusGuard { + /// Captures the current instrumented span. + pub fn current() -> Self { + Self { + span: tracing::Span::current(), + finished: false, + } + } + + /// Returns `result`, marking this guard complete when it is successful. + pub fn finish(mut self, result: Result) -> Result { + self.finished = result.is_ok(); + result + } +} + +impl Drop for ErrorStatusGuard { + fn drop(&mut self) { + if !self.finished { + mark_error(&self.span); + } + } +} + +/// How a process chooses its OpenTelemetry `service.name`. +#[derive(Debug, Clone, Copy)] +pub enum ServiceName<'a> { + /// Always use this name, overriding `OTEL_SERVICE_NAME`. + Fixed(&'a str), + /// Use `OTEL_SERVICE_NAME` when set, otherwise use this default. + EnvironmentOr(&'a str), +} + +/// OTLP transport to a collector. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum Protocol { + #[default] + Grpc, + HttpProtobuf, +} + +/// Inputs for OTLP export. +#[derive(Debug, Clone)] +pub struct OtlpExportConfig<'a> { + pub endpoint: &'a str, + pub service_name: ServiceName<'a>, + pub service_version: Option<&'a str>, + pub resource_attributes: Vec, +} + +/// How exports reach a collector: the transport, request headers, and TLS. +#[derive(Clone, Default, PartialEq, Eq)] +pub struct CollectorTransport { + pub protocol: Protocol, + pub headers: http::HeaderMap, + pub tls: OtlpTls, +} + +impl std::fmt::Debug for CollectorTransport { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("CollectorTransport") + .field("protocol", &self.protocol) + .field("header_names", &self.headers.keys().collect::>()) + .field("tls", &self.tls) + .finish() + } +} + +/// Failure to set up OTLP export. +#[derive(Debug, thiserror::Error)] +pub enum SetupError { + #[error("OTLP endpoint is empty")] + EmptyEndpoint, + + #[error("invalid OTLP endpoint {endpoint:?}: {source}")] + InvalidEndpoint { + endpoint: String, + source: http::uri::InvalidUri, + }, + + #[error("failed to build the OTLP exporter: {0}")] + Exporter(#[from] opentelemetry_otlp::ExporterBuildError), + + #[error("invalid OTLP endpoint {endpoint:?}: {source}")] + InvalidSignalUrl { + endpoint: String, + source: http::Error, + }, + + #[error("OTLP TLS settings need an https:// endpoint, not {endpoint:?}")] + TlsWithoutHttps { endpoint: String }, + + #[error("invalid OTLP TLS settings: {0}")] + Tls(#[from] TlsError), + + #[error("invalid OTLP TLS files: {0}")] + TlsFiles(#[from] TlsFileError), + + #[error("OTLP/HTTP export needs a tokio runtime")] + NoRuntime, +} + +fn resource_attributes(config: &OtlpExportConfig<'_>) -> Vec { + let mut attributes = config.resource_attributes.clone(); + if let Some(version) = config + .service_version + .map(str::trim) + .filter(|version| !version.is_empty()) + { + attributes.push(KeyValue::new("service.version", version.to_string())); + } + attributes +} + +/// Build the OpenTelemetry resource for an export configuration. +#[must_use] +pub fn resource_for(config: &OtlpExportConfig<'_>) -> Resource { + let attributes = resource_attributes(config); + match config.service_name { + ServiceName::Fixed(name) => Resource::builder() + .with_service_name(name.trim().to_string()) + .with_attributes(attributes) + .build(), + ServiceName::EnvironmentOr(default) => { + let detected = Resource::builder() + .with_attributes(attributes.clone()) + .build(); + if detected + .get(&opentelemetry::Key::from_static_str("service.name")) + .is_some_and(|value| !value.to_string().starts_with(SDK_UNKNOWN_SERVICE_PREFIX)) + { + detected + } else { + Resource::builder() + .with_service_name(default.trim().to_string()) + .with_attributes(attributes) + .build() + } + } + } +} + +/// Trim and validate an OTLP endpoint before handing it to an exporter. +/// +/// Every exporter that reads an operator-supplied endpoint uses this, so a +/// value accepted for one signal is accepted for all of them. +pub fn validated_endpoint(endpoint: &str) -> Result<(&str, http::Uri), SetupError> { + let endpoint = endpoint.trim(); + if endpoint.is_empty() { + return Err(SetupError::EmptyEndpoint); + } + let uri = endpoint + .parse::() + .map_err(|source| SetupError::InvalidEndpoint { + endpoint: endpoint.to_string(), + source, + })?; + Ok((endpoint, uri)) +} + +/// A validated OTLP collector endpoint and the resource that every signal +/// exported to it describes. +#[derive(Clone)] +pub struct OtlpTarget { + pub(crate) endpoint: String, + pub(crate) tls: bool, + pub(crate) protocol: Protocol, + pub(crate) headers: http::HeaderMap, + pub(crate) tls_settings: OtlpTls, + pub(crate) resource: Resource, +} + +impl std::fmt::Debug for OtlpTarget { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("OtlpTarget") + .field("endpoint", &self.endpoint) + .field("tls", &self.tls) + .field("protocol", &self.protocol) + .field("header_names", &self.headers.keys().collect::>()) + .field("tls_settings", &self.tls_settings) + .field("resource", &self.resource) + .finish() + } +} + +impl OtlpTarget { + /// Validate the endpoint and build the resource. + pub fn new(config: &OtlpExportConfig<'_>) -> Result { + let (endpoint, uri) = validated_endpoint(config.endpoint)?; + Ok(Self { + endpoint: endpoint.to_string(), + tls: uri.scheme() == Some(&http::uri::Scheme::HTTPS), + protocol: Protocol::Grpc, + headers: http::HeaderMap::new(), + tls_settings: OtlpTls::default(), + resource: resource_for(config), + }) + } + + /// Export over `protocol` instead of gRPC. + #[must_use] + pub fn with_protocol(mut self, protocol: Protocol) -> Self { + self.protocol = protocol; + self + } + + /// Send `headers` with every export request. Headers from the process + /// environment (`OTEL_EXPORTER_OTLP_*HEADERS`) still apply and replace + /// any of the same name. + #[must_use] + pub fn with_headers(mut self, headers: http::HeaderMap) -> Self { + self.headers = headers; + self + } + + /// Reach the collector the way `transport` describes. + #[must_use] + pub fn with_transport(self, transport: &CollectorTransport) -> Self { + self.with_protocol(transport.protocol) + .with_headers(transport.headers.clone()) + .with_tls(transport.tls.clone()) + } + + /// Verify an `https://` collector and authenticate to it with `tls`. + #[must_use] + pub fn with_tls(mut self, tls: OtlpTls) -> Self { + self.tls_settings = tls; + self + } + + /// The URL of `signal_path` under the endpoint, for OTLP/HTTP export. + pub(crate) fn http_signal_url(&self, signal_path: &str) -> Result { + signal_url(&self.endpoint, signal_path) + .map(|url| url.to_string()) + .map_err(|source| SetupError::InvalidSignalUrl { + endpoint: self.endpoint.clone(), + source, + }) + } + + /// Point an exporter builder at this target, over TLS for `https://`. + pub(crate) fn configure(&self, builder: B) -> B + where + B: WithExportConfig + WithTonicConfig, + { + let builder = builder + .with_endpoint(self.endpoint.clone()) + .with_metadata(MetadataMap::from_headers(self.headers.clone())); + if self.tls { + builder.with_tls_config(self.tls_settings.grpc_config()) + } else { + builder + } + } + + /// Point an OTLP/HTTP exporter builder at `signal_path` under this + /// target. + pub(crate) fn configure_http( + &self, + builder: B, + signal_path: &str, + timeout_var: &str, + ) -> Result + where + B: WithExportConfig + WithHttpConfig, + { + // The SDK percent-decodes these values again. + let headers = self + .headers + .iter() + .filter_map(|(name, value)| { + Some((name.to_string(), value.to_str().ok()?.replace('%', "%25"))) + }) + .collect(); + Ok(builder + .with_protocol(opentelemetry_otlp::Protocol::HttpBinary) + .with_endpoint(self.http_signal_url(signal_path)?) + .with_http_client(OtlpHttpClient::new( + self.tls.then_some(&self.tls_settings), + transport::export_timeout(timeout_var), + )?) + .with_headers(headers)) + } + + #[must_use] + pub fn endpoint(&self) -> &str { + &self.endpoint + } + + #[must_use] + pub fn resource(&self) -> &Resource { + &self.resource + } + + /// Fails when TLS settings are given for a plaintext endpoint. + pub(crate) fn check_tls(&self) -> Result<(), SetupError> { + if self.tls || self.tls_settings.is_default() { + Ok(()) + } else { + Err(SetupError::TlsWithoutHttps { + endpoint: self.endpoint.clone(), + }) + } + } + + /// Build a trace provider that exports spans to this target in batches. + pub fn tracer_provider(&self) -> Result { + self.check_tls()?; + let exporter = match self.protocol { + Protocol::Grpc => self + .configure(SpanExporter::builder().with_tonic()) + .build()?, + Protocol::HttpProtobuf => self + .configure_http( + SpanExporter::builder().with_http(), + "v1/traces", + "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", + )? + .build()?, + }; + Ok(SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(self.resource.clone()) + .build()) + } +} + +/// Build an OTLP trace provider. +pub fn build_provider(config: &OtlpExportConfig<'_>) -> Result { + OtlpTarget::new(config)?.tracer_provider() +} + +/// Build the provider for an optional OTLP configuration. +/// +/// Telemetry setup failures disable export and remain available for the caller +/// to report after its tracing subscriber is installed. +#[must_use] +pub fn provider_for( + config: Option>, +) -> (Option, Option) { + match config.as_ref().map(build_provider) { + None => (None, None), + Some(Ok(provider)) => (Some(provider), None), + Some(Err(error)) => (None, Some(error)), + } +} + +/// Filtered OpenTelemetry layer returned by [`layer`]. +pub type OtlpLayer = tracing_subscriber::filter::Filtered< + OpenTelemetryLayer, + tracing_subscriber::filter::FilterFn, + S, +>; + +pub type TargetOtlpLayer = + tracing_subscriber::filter::Filtered, TargetPrefixFilter, S>; + +#[derive(Debug, Clone)] +pub struct TargetPrefixFilter { + prefixes: Vec<&'static str>, + include: bool, +} + +impl Filter for TargetPrefixFilter { + fn enabled(&self, metadata: &tracing::Metadata<'_>, _ctx: &Context<'_, S>) -> bool { + let matches_prefix = self + .prefixes + .iter() + .any(|prefix| metadata.target().starts_with(prefix)); + metadata.is_span() + && !metadata.target().starts_with("opentelemetry") + && (matches_prefix == self.include) + } +} + +/// Build a tracing layer that exports spans and excludes exporter callsites. +pub fn layer(provider: &SdkTracerProvider, instrumentation_scope: &'static str) -> OtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(tracing_subscriber::filter::filter_fn(|metadata| { + metadata.is_span() && !metadata.target().starts_with("opentelemetry") + })) +} + +/// Build a tracing layer that exports only spans from `target_prefix`. +pub fn layer_for_target_prefix( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefix: &'static str, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + layer_for_target_prefixes(provider, instrumentation_scope, [target_prefix]) +} + +/// Build a tracing layer that exports only spans from `target_prefixes`. +pub fn layer_for_target_prefixes( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefixes: impl IntoIterator, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(TargetPrefixFilter { + prefixes: target_prefixes.into_iter().collect(), + include: true, + }) +} + +/// Build a tracing layer that excludes spans from `target_prefix`. +/// +/// `None` excludes no application spans. +pub fn layer_excluding_target_prefix( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefix: Option<&'static str>, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + layer_excluding_target_prefixes(provider, instrumentation_scope, target_prefix) +} + +/// Build a tracing layer that excludes spans from `target_prefixes`. +/// +/// An empty iterator excludes no application spans. +pub fn layer_excluding_target_prefixes( + provider: &SdkTracerProvider, + instrumentation_scope: &'static str, + target_prefixes: impl IntoIterator, +) -> TargetOtlpLayer +where + S: Subscriber + for<'span> LookupSpan<'span>, +{ + tracing_opentelemetry::layer() + .with_tracer(provider.tracer(instrumentation_scope)) + .with_filter(TargetPrefixFilter { + prefixes: target_prefixes.into_iter().collect(), + include: false, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse_otlp_headers; + + #[test] + fn supervisor_span_filter_raises_only_openshell_targets() { + let directives = |log_level: &str| supervisor_span_filter(log_level).to_string(); + assert_eq!(directives("warn"), "openshell=info,info"); + assert_eq!(directives("info"), "openshell=info,info"); + assert_eq!(directives("debug"), "openshell=debug,info"); + assert_eq!(directives("info,h2=debug"), "openshell=info,info"); + } + + #[test] + fn error_status_guard_marks_only_unfinished_results() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry().with(layer(&provider, "guard-test")); + + tracing::subscriber::with_default(subscriber, || { + let failed = + tracing::info_span!("failed-operation", otel.status_code = tracing::field::Empty); + { + let _entered = failed.enter(); + drop(ErrorStatusGuard::current()); + } + drop(failed); + + let succeeded = tracing::info_span!( + "successful-operation", + otel.status_code = tracing::field::Empty + ); + { + let _entered = succeeded.enter(); + ErrorStatusGuard::current().finish(Ok::<_, ()>(())).unwrap(); + } + drop(succeeded); + }); + + let spans = exporter.get_finished_spans().unwrap(); + let failed = spans + .iter() + .find(|span| span.name == "failed-operation") + .unwrap(); + assert!(matches!( + failed.status, + opentelemetry::trace::Status::Error { .. } + )); + let succeeded = spans + .iter() + .find(|span| span.name == "successful-operation") + .unwrap(); + assert_eq!(succeeded.status, opentelemetry::trace::Status::Unset); + } + + #[test] + fn target_scoped_layers_partition_in_process_driver_spans() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let gateway_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let gateway_provider = SdkTracerProvider::builder() + .with_simple_exporter(gateway_exporter.clone()) + .build(); + let driver_exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let driver_provider = SdkTracerProvider::builder() + .with_simple_exporter(driver_exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(layer_excluding_target_prefix( + &gateway_provider, + "gateway", + Some("driver_target"), + )) + .with(layer_for_target_prefix( + &driver_provider, + "driver", + "driver_target", + )); + + tracing::subscriber::with_default(subscriber, || { + let gateway = tracing::info_span!(target: "gateway_target", "gateway.span"); + let _entered = gateway.enter(); + drop(tracing::info_span!(target: "driver_target::operation", "driver.span")); + }); + + gateway_provider.force_flush().unwrap(); + driver_provider.force_flush().unwrap(); + let gateway_spans = gateway_exporter.get_finished_spans().unwrap(); + let driver_spans = driver_exporter.get_finished_spans().unwrap(); + assert_eq!( + gateway_spans + .iter() + .map(|span| span.name.as_ref()) + .collect::>(), + ["gateway.span"] + ); + assert_eq!( + driver_spans + .iter() + .map(|span| span.name.as_ref()) + .collect::>(), + ["driver.span"] + ); + assert_eq!( + driver_spans[0].span_context.trace_id(), + gateway_spans[0].span_context.trace_id() + ); + assert_eq!( + driver_spans[0].parent_span_id, + gateway_spans[0].span_context.span_id() + ); + } + + #[test] + fn excluding_no_target_prefix_exports_all_application_spans() { + use tracing_subscriber::layer::SubscriberExt as _; + + let _tracing_lock = crate::test_lock(); + let exporter = opentelemetry_sdk::trace::InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + let subscriber = tracing_subscriber::registry() + .with(layer_excluding_target_prefix(&provider, "gateway", None)); + + tracing::subscriber::with_default(subscriber, || { + drop(tracing::info_span!(target: "\0driver_target", "nul-prefixed.span")); + drop(tracing::info_span!(target: "gateway_target", "gateway.span")); + }); + + provider.force_flush().unwrap(); + let spans = exporter.get_finished_spans().unwrap(); + assert_eq!(spans.len(), 2); + assert!(spans.iter().any(|span| span.name == "nul-prefixed.span")); + assert!(spans.iter().any(|span| span.name == "gateway.span")); + } + + #[test] + fn resource_uses_fixed_service_identity_and_custom_attributes() { + let resource = resource_for(&OtlpExportConfig { + endpoint: "http://127.0.0.1:4317", + service_name: ServiceName::Fixed("openshell-driver-vm"), + service_version: Some("1.2.3"), + resource_attributes: vec![KeyValue::new("openshell.gateway.name", "vm-dev")], + }); + + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str("service.name")) + .map(|value| value.to_string()), + Some("openshell-driver-vm".to_string()) + ); + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str("service.version")) + .map(|value| value.to_string()), + Some("1.2.3".to_string()) + ); + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str( + "openshell.gateway.name", + )) + .map(|value| value.to_string()), + Some("vm-dev".to_string()) + ); + } + + #[tokio::test] + async fn tls_settings_need_an_https_endpoint() { + let pki = openshell_otel_test_support::TestPki::generate(); + let tls = OtlpTls::from_pem(Some(pki.ca_pem.as_bytes()), None).unwrap(); + for protocol in [Protocol::Grpc, Protocol::HttpProtobuf] { + let target = |endpoint| { + OtlpTarget::new(&OtlpExportConfig { + endpoint, + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_protocol(protocol) + .with_tls(tls.clone()) + }; + assert!(matches!( + target("http://collector:4317").tracer_provider(), + Err(SetupError::TlsWithoutHttps { .. }) + )); + assert!(matches!( + target("http://collector:4317").log_exporter(), + Err(SetupError::TlsWithoutHttps { .. }) + )); + assert!(target("https://collector:4317").tracer_provider().is_ok()); + } + } + + #[test] + fn debug_output_names_headers_without_their_values() { + let target = OtlpTarget::new(&OtlpExportConfig { + endpoint: "http://collector:4317", + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_headers(parse_otlp_headers("authorization=Bearer%20secret-token")); + + let debug = format!("{target:?}"); + assert!(debug.contains("authorization"), "{debug}"); + assert!(!debug.contains("secret-token"), "{debug}"); + } + + #[tokio::test(flavor = "multi_thread")] + async fn trace_provider_sends_headers_as_grpc_metadata() { + crate::test_environment::Environment::new() + .remove("OTEL_EXPORTER_OTLP_HEADERS") + .remove("OTEL_EXPORTER_OTLP_TRACES_HEADERS") + .remove("OTEL_EXPORTER_OTLP_LOGS_HEADERS") + .run_async(async { + use opentelemetry::trace::Tracer as _; + + let collector = openshell_otel_test_support::OtlpTestServer::start().await; + let provider = OtlpTarget::new(&OtlpExportConfig { + endpoint: collector.endpoint(), + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + }) + .unwrap() + .with_headers(parse_otlp_headers("authorization=Bearer%20token")) + .tracer_provider() + .unwrap(); + provider.tracer("test").in_span("grpc-span", |_| {}); + let flush = tokio::task::spawn_blocking(move || provider.force_flush()); + + collector.wait_for_export().await; + flush.await.unwrap().expect("flush succeeds"); + assert_eq!(collector.received().trace_authorizations, ["Bearer token"]); + }) + .await; + } + + #[tokio::test] + async fn malformed_endpoint_disables_export_with_a_reportable_error() { + let (provider, error) = provider_for(Some(OtlpExportConfig { + endpoint: "definitely not a url", + service_name: ServiceName::Fixed("test-service"), + service_version: None, + resource_attributes: Vec::new(), + })); + + assert!(provider.is_none()); + assert!(matches!(error, Some(SetupError::InvalidEndpoint { .. }))); + } +} diff --git a/crates/openshell-sdk/src/client.rs b/crates/openshell-sdk/src/client.rs index 1f03916ef2..8241ac49b5 100644 --- a/crates/openshell-sdk/src/client.rs +++ b/crates/openshell-sdk/src/client.rs @@ -89,6 +89,16 @@ impl OpenShellClient { self.channel.clone() } + /// Clone of the auth interceptor backing [`OpenShellClient::raw_grpc`]. + /// + /// tonic allows a single interceptor per channel, so callers that need + /// to stack another concern (e.g. trace-context forwarding) on top of + /// authentication can compose around this clone — it shares the same + /// live bearer slot, so OIDC refresh still reaches the composed stack. + pub fn auth_interceptor(&self) -> EdgeAuthInterceptor { + self.interceptor.clone() + } + /// Authenticated gRPC client for the main `OpenShell` service. /// /// Use this when the curated surface below doesn't expose the RPC or @@ -1770,6 +1780,31 @@ mod tests { ); } + #[tokio::test] + async fn auth_interceptor_clone_sees_refreshed_token() { + use tonic::service::Interceptor as _; + + // The clone handed out by `auth_interceptor()` must share the live + // bearer slot: callers compose other concerns (e.g. the CLI's + // trace-forwarding gateway interceptor) around it, and a forced OIDC + // refresh must reach those already-built stacks. + let calls = Arc::new(AtomicUsize::new(0)); + let client = oidc_client_with_refresher(Arc::clone(&calls)); + let mut interceptor = client.auth_interceptor(); + + assert!(client.force_refresh().await.unwrap()); + + let request = interceptor.call(tonic::Request::new(())).unwrap(); + assert_eq!( + request + .metadata() + .get("authorization") + .and_then(|value| value.to_str().ok()), + Some("Bearer token-1"), + "refreshed token must reach the cloned interceptor" + ); + } + #[test] fn store_bearer_rejects_malformed_token_and_keeps_previous() { let slot: BearerSlot = Arc::new(RwLock::new(None)); diff --git a/docs/observability/traces.mdx b/docs/observability/traces.mdx index 0cf8ca3c27..3cbdba70a3 100644 --- a/docs/observability/traces.mdx +++ b/docs/observability/traces.mdx @@ -71,6 +71,8 @@ Give each installation that reports to the same collector its own name, includin The gateway continues a valid W3C `traceparent` sent by the caller, so a client's trace can include the gateway's spans for its requests. Without one, the gateway starts a new trace. +The CLI forwards `TRACEPARENT` and `TRACESTATE` from its environment on gateway requests from its standard commands. A CI pipeline instrumented with an OpenTelemetry integration that exports these variables (for example the Jenkins OpenTelemetry plugin or an OpenTelemetry GitHub Action) therefore parents the gateway's spans for the commands it runs, with no collector or provider configuration on the CI runner. + The gateway also passes its trace context to compute drivers, so driver spans appear in the same trace as the gateway request that caused them. Built-in drivers export their spans through the gateway's process. Managed external drivers receive the trace endpoint, gateway name, and compute driver from the gateway. Drivers that you run yourself own their telemetry configuration. ## Next Steps From 00f7ba72bf084d95c877c583882e7b376e3ea54b Mon Sep 17 00:00:00 2001 From: Shiju Date: Fri, 6 Nov 2026 20:33:22 +0000 Subject: [PATCH 26/31] fix(tui): show MCP tool selectors in the policy viewer (#5345) * fix(tui): show MCP tool selectors in policy viewer Signed-off-by: Shiju * chore(tui): keep policy display fix scoped to code Signed-off-by: Shiju --------- Signed-off-by: Shiju --- crates/openshell-tui/src/lib.rs | 223 ++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 133d7d5c0a..0e807cd452 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1315,6 +1315,16 @@ fn render_policy_lines( // L7 allow rules. for l7 in &ep.rules { if let Some(allow) = &l7.allow { + if ep.protocol.eq_ignore_ascii_case("mcp") { + render_mcp_rule( + &mut lines, + "Allow", + &allow.method, + allow.params.get("name"), + t, + ); + continue; + } let method = if allow.method.is_empty() { "*" } else { @@ -1334,6 +1344,18 @@ fn render_policy_lines( } } + if ep.protocol.eq_ignore_ascii_case("mcp") { + for deny in &ep.deny_rules { + render_mcp_rule( + &mut lines, + "Deny", + &deny.method, + deny.params.get("name"), + t, + ); + } + } + // Access preset (if set instead of explicit rules). if ep.access != 0 && ep.rules.is_empty() { lines.push(Line::from(vec![ @@ -1371,6 +1393,37 @@ fn render_policy_lines( lines } +fn render_mcp_rule( + lines: &mut Vec>, + action: &str, + method: &str, + tool: Option<&openshell_core::proto::L7QueryMatcher>, + theme: &theme::Theme, +) { + use ratatui::text::{Line, Span}; + + let method = if method.is_empty() { + if tool.is_some() { "tools/call" } else { "*" } + } else { + method + }; + lines.push(Line::from(vec![ + Span::styled(format!(" {action}: "), theme.muted), + Span::styled(method.to_string(), theme.text), + ])); + if let Some(tool) = tool { + for name in std::iter::once(&tool.glob) + .filter(|name| !name.is_empty()) + .chain(&tool.any) + { + lines.push(Line::from(vec![ + Span::styled(" Tool: ", theme.muted), + Span::styled(name.clone(), theme.text), + ])); + } + } +} + // --------------------------------------------------------------------------- // Animation helper // --------------------------------------------------------------------------- @@ -3444,3 +3497,173 @@ mod sandbox_notes_tests { assert_eq!(sandbox_notes(&sandbox, String::new()), ""); } } + +#[cfg(test)] +mod policy_render_tests { + use super::*; + + fn policy_lines(yaml: &str) -> Vec> { + let policy = openshell_policy::parse_sandbox_policy(yaml).expect("valid policy"); + render_policy_lines(&policy, &theme::Theme::dark()) + } + + #[test] + fn mcp_tool_allowlist_is_visible_one_tool_per_line() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: host.openshell.internal + port: 32443 + protocol: mcp + enforcement: enforce + rules: + - allow: + method: tools/call + tool: + any: + - local_gh_issue_view + - local_gh_pr_checks + - local_gh_pr_details + - local_gh_pr_diff + - local_gh_pr_list + - local_gh_pr_view + - local_gh_repo_view + - local_gh_run_failed_logs + - local_gh_run_view + - local_gh_source_blob + - local_gh_source_tree + - local_gh_version +", + ); + let tools: Vec<_> = lines + .iter() + .map(ToString::to_string) + .filter_map(|line| line.strip_prefix(" Tool: ").map(str::to_string)) + .collect(); + assert_eq!( + tools, + [ + "local_gh_issue_view", + "local_gh_pr_checks", + "local_gh_pr_details", + "local_gh_pr_diff", + "local_gh_pr_list", + "local_gh_pr_view", + "local_gh_repo_view", + "local_gh_run_failed_logs", + "local_gh_run_view", + "local_gh_source_blob", + "local_gh_source_tree", + "local_gh_version", + ] + ); + assert!( + lines + .iter() + .any(|line| line.to_string() == " Allow: tools/call") + ); + assert!( + !lines + .iter() + .any(|line| line.to_string().contains("tools/call *")) + ); + + let mut buffer = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(0, 0, 60, 30)); + ratatui::widgets::Widget::render( + ratatui::widgets::Paragraph::new(lines), + buffer.area, + &mut buffer, + ); + for tool in tools { + assert!(buffer.content.chunks(60).any(|row| { + row.iter() + .map(ratatui::buffer::Cell::symbol) + .collect::() + .contains(&tool) + })); + } + } + + #[test] + fn mcp_methods_and_allow_deny_selectors_preserve_their_scope() { + let lines = policy_lines( + r" +version: 1 +network_policies: + review: + endpoints: + - host: mcp.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: + method: tools/list + - allow: + tool: read_source + - allow: + method: tools/call + params: + name: + any: [read_*, list_*] + deny_rules: + - tool: read_private + - method: tools/call + params: + name: + any: [read_secret_*, list_private] + - method: resources/read + profile: + endpoints: + - host: profile.example.com + port: 443 + protocol: mcp + mcp: + allow_all_known_mcp_methods: true + rules: + - allow: {} + deny_rules: + - {} + rest: + endpoints: + - host: api.example.com + port: 443 + protocol: rest + rules: + - allow: {method: GET, path: /repos/**} +", + ); + let text: Vec<_> = lines.iter().map(ToString::to_string).collect(); + assert!(text.iter().any(|line| line == " Allow: *")); + assert!(text.iter().any(|line| line == " Deny: *")); + let mcp_start = text + .iter() + .position(|line| line == " Allow: tools/list") + .unwrap(); + assert_eq!( + &text[mcp_start..mcp_start + 12], + [ + " Allow: tools/list", + " Allow: tools/call", + " Tool: read_source", + " Allow: tools/call", + " Tool: read_*", + " Tool: list_*", + " Deny: tools/call", + " Tool: read_private", + " Deny: tools/call", + " Tool: read_secret_*", + " Tool: list_private", + " Deny: resources/read", + ] + ); + assert!( + text.iter() + .any(|line| line == " Allow: GET /repos/**") + ); + } +} From cb836ea9d78cade7502167b2569a952e5e715cdc Mon Sep 17 00:00:00 2001 From: Kris Hicks Date: Fri, 6 Nov 2026 21:20:47 +0000 Subject: [PATCH 27/31] fix(ci): enable OTLP export only when a collector is requested (#5348) Kubernetes E2E layered ci/values-skaffold.yaml, which pointed the gateway at the openshell-collector service that only the local k3d workflow installs. Kind lanes logged a BatchSpanProcessor.ExportError every batch. The local gateway tasks probed 127.0.0.1:4317 instead, and the gateway task configured the endpoint unconditionally. Move the collector endpoint into ci/values-local-tracing.yaml, layered only by skaffold.yaml, and have every local gateway task enable OTLP export only when OPENSHELL_DEV_OTLP_ENDPOINT is set. Closes #5347 Refs #3767 Signed-off-by: Kris Hicks --- .agents/skills/helm-dev-environment/SKILL.md | 13 ++++++---- crates/openshell-driver-vm/README.md | 2 +- .../openshell/ci/values-local-tracing.yaml | 10 ++++++++ deploy/helm/openshell/ci/values-skaffold.yaml | 6 ----- deploy/helm/openshell/skaffold.yaml | 1 + skills/debug-openshell-cluster/SKILL.md | 2 +- tasks/scripts/gateway-docker.sh | 24 ++----------------- tasks/scripts/gateway-podman.sh | 24 ++----------------- tasks/scripts/gateway-toml.sh | 24 +++++++++++++++++++ tasks/scripts/gateway-vm.sh | 24 ++----------------- tasks/scripts/gateway.sh | 16 ++++++------- 11 files changed, 58 insertions(+), 88 deletions(-) create mode 100644 deploy/helm/openshell/ci/values-local-tracing.yaml diff --git a/.agents/skills/helm-dev-environment/SKILL.md b/.agents/skills/helm-dev-environment/SKILL.md index e866ec6927..56061385c3 100644 --- a/.agents/skills/helm-dev-environment/SKILL.md +++ b/.agents/skills/helm-dev-environment/SKILL.md @@ -266,8 +266,9 @@ task running while using those endpoints. ### Viewing local traces The gateway exports OTLP/gRPC to -`http://openshell-collector.observability.svc.cluster.local:4317` through the -default Skaffold values. Forward OTLP/gRPC and the trace UI to the host: +`http://openshell-collector.observability.svc.cluster.local:4317` through +`ci/values-local-tracing.yaml`, which only `skaffold.yaml` layers. The Kubernetes +E2E wrapper omits it, so E2E clusters export no traces. Forward OTLP/gRPC and the trace UI to the host: ```bash mise run helm:k3s:forward @@ -278,9 +279,10 @@ Kubernetes compute-driver spans under their distinct service names, along with Agent Sandbox controller reconciliation spans linked through the Sandbox trace-context annotation. The same command exposes OTLP/gRPC on `http://127.0.0.1:4317` and, when deployed, the Kubernetes gateway on -`http://127.0.0.1:8090`. The local `gateway:docker`, `gateway:podman`, and -`gateway:vm` tasks detect the collector listener at startup and enable trace -export, OCSF log export, and the agent trace relay only while it is reachable. +`http://127.0.0.1:8090`. The local `gateway`, `gateway:docker`, +`gateway:podman`, and `gateway:vm` tasks enable trace export, OCSF log export, +and the agent trace relay only when `OPENSHELL_DEV_OTLP_ENDPOINT` is set, for +example `OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317`. Spans an agent exports from inside a sandbox arrive through the supervisor's relay with `openshell.telemetry.source=agent` and share `openshell.sandbox.id` with that sandbox's supervisor spans. @@ -642,6 +644,7 @@ for dependencies still declared in `Chart.yaml`. | `deploy/helm/openshell/skaffold.yaml` | Skaffold config — images, Helm releases, values overlays | | `deploy/helm/openshell/values.yaml` | Default Helm values | | `deploy/helm/openshell/ci/values-skaffold.yaml` | Dev overrides (image pull policy, TLS disabled for local Skaffold) | +| `deploy/helm/openshell/ci/values-local-tracing.yaml` | OTLP export to the `helm:k3s:create` collector (Skaffold only) | | `deploy/helm/openshell/ci/values-cert-manager.yaml` | cert-manager PKI overlay (opt-in; disables pkiInitJob) | | `deploy/helm/openshell/ci/values-gateway.yaml` | Envoy Gateway GRPCRoute + Gateway overlay | | `deploy/helm/openshell/ci/values-high-availability.yaml` | HA test overlay (`replicaCount: 2` with external PostgreSQL Secret) | diff --git a/crates/openshell-driver-vm/README.md b/crates/openshell-driver-vm/README.md index a0371bacfb..cb9433d08e 100644 --- a/crates/openshell-driver-vm/README.md +++ b/crates/openshell-driver-vm/README.md @@ -70,7 +70,7 @@ By default `mise run gateway:vm`: - Persists the gateway SQLite DB under `.cache/gateway-vm/gateway.db`. - Places the VM driver state (per-sandbox `overlay.ext4`, image cache, and `run/compute-driver.sock`) under `/tmp/openshell-vm-driver-$USER-vm-dev/` so the AF_UNIX socket path stays under macOS `SUN_LEN`. - Writes `.cache/gateway-vm/gateway.toml` with `[openshell.drivers.vm].driver_dir = "$PWD/target/debug"` so the freshly built `openshell-driver-vm` is used instead of an older installed copy from `~/.local/libexec/openshell`, `/usr/libexec/openshell`, or `/usr/local/libexec`. -- Enables OTLP trace export, OCSF log export, and the agent trace relay to `http://127.0.0.1:4317` only when a local collector is listening there. Otherwise, it omits the OTLP configuration to avoid repeated export failures. +- Enables OTLP trace export, OCSF log export, and the agent trace relay only when `OPENSHELL_DEV_OTLP_ENDPOINT` names a collector, for example `http://127.0.0.1:4317`. Otherwise, it omits the OTLP configuration. For GPU passthrough (VFIO), pass `-- --gpu` and run with root privileges: diff --git a/deploy/helm/openshell/ci/values-local-tracing.yaml b/deploy/helm/openshell/ci/values-local-tracing.yaml new file mode 100644 index 0000000000..68d1f23500 --- /dev/null +++ b/deploy/helm/openshell/ci/values-local-tracing.yaml @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Export gateway traces to the collector that `mise run helm:k3s:create` +# installs. Layer only onto clusters that run that collector. +gatewayConfig: + openshell.gateway.otlp: + endpoint: http://openshell-collector.observability.svc.cluster.local:4317 + openshell.gateway.otlp.agent: + enabled: true diff --git a/deploy/helm/openshell/ci/values-skaffold.yaml b/deploy/helm/openshell/ci/values-skaffold.yaml index 1ff2bb2fc4..76e0167e79 100644 --- a/deploy/helm/openshell/ci/values-skaffold.yaml +++ b/deploy/helm/openshell/ci/values-skaffold.yaml @@ -5,16 +5,10 @@ gatewayConfig: openshell.drivers.kubernetes: image_pull_policy: if_not_present - openshell.gateway.otlp: - endpoint: http://openshell-collector.observability.svc.cluster.local:4317 - openshell.gateway.otlp.agent: - enabled: true openshell.gateway.auth: allow_unauthenticated_users: true server: - otlp: - endpoint: http://openshell-collector.observability.svc.cluster.local:4317 # Comment out to enforce mTLS (uses PKI secrets generated by pkiInitJob). disableTls: true auth: diff --git a/deploy/helm/openshell/skaffold.yaml b/deploy/helm/openshell/skaffold.yaml index 3ad4d3b8bf..f505c479fa 100644 --- a/deploy/helm/openshell/skaffold.yaml +++ b/deploy/helm/openshell/skaffold.yaml @@ -124,6 +124,7 @@ deploy: valuesFiles: - values.yaml - ci/values-skaffold.yaml + - ci/values-local-tracing.yaml # Add ci/values-cert-manager.yaml here (and uncomment the cert-manager # release above) to switch TLS generation to cert-manager. #- ci/values-cert-manager.yaml diff --git a/skills/debug-openshell-cluster/SKILL.md b/skills/debug-openshell-cluster/SKILL.md index 3e75574b8e..23d58efa25 100644 --- a/skills/debug-openshell-cluster/SKILL.md +++ b/skills/debug-openshell-cluster/SKILL.md @@ -1590,7 +1590,7 @@ credential failures. | `openshell status` fails | Gateway endpoint unreachable or auth mismatch | `openshell gateway info`, gateway logs | | Collector receives no OCSF log records, or `openshell.ocsf.gap` records | `[openshell.gateway.otlp.ocsf]` missing or without `enabled = true`, an unreachable endpoint or one that does not speak the table's `protocol`, or export failures and queue pressure | Check `[openshell.gateway.otlp.ocsf]` for `enabled = true` and the endpoint it resolves to, the gateway's `OTLP exporting enabled` startup log for `ocsf_endpoint` and `ocsf_protocol`, export warnings, and `openshell_ocsf_otlp_*` metrics. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/how-it-works/gateways/configuration). | | Gateway OCSF JSONL stops growing, has gaps, or is rejected by a SIEM | File errors, queue pressure, or shipper falling behind retention | Inspect gateway warnings and `openshell_ocsf_log_*` metrics; check `[openshell.gateway.ocsf_log]`, directory permissions, free space, per-replica paths, and shipper rotation checkpoints. See the published [gateway configuration reference](https://docs.nvidia.com/openshell/latest/how-it-works/gateways/configuration). | -| `BatchSpanProcessor.ExportError` repeatedly reports connection refused on `127.0.0.1:4317` | The local gateway started with OTLP configured but the collector forwarding task later stopped, or the config was created manually | Restart `gateway:docker`, `gateway:podman`, or `gateway:vm` so it re-detects the listener; inspect the generated `gateway.toml` for `[openshell.gateway.otlp]` | +| `BatchSpanProcessor.ExportError` repeatedly reports connection refused on `127.0.0.1:4317` | The local gateway started with `OPENSHELL_DEV_OTLP_ENDPOINT` set but no collector is listening, for example because `helm:k3s:forward` stopped | Start the collector forwarding again, or restart the gateway task without `OPENSHELL_DEV_OTLP_ENDPOINT`; inspect the generated `gateway.toml` for `[openshell.gateway.otlp]` | | Sandbox log shows `OTLP agent trace relay inactive: not enabled by the gateway` at startup, then `agent OTLP export attempted but the agent trace relay is not enabled` when the agent exports | The gateway does not enable `[openshell.gateway.otlp.agent]`, so the supervisor refuses the workload's OTLP relay address | Set `enabled = true` in `[openshell.gateway.otlp.agent]` (Helm: `gatewayConfig."openshell.gateway.otlp.agent".enabled`) with an endpoint from that table or `[openshell.gateway.otlp]`, and recreate the sandbox; `openshell logs --source sandbox` then shows `OTLP agent trace relay enabled` | | Gateway starts but sandbox create fails | Compute driver cannot reach runtime | Docker/Podman/Kubernetes/VM driver logs | | Docker or Podman sandbox never registers | Wrong gateway endpoint, unavailable host networking, or supervisor startup failure | Gateway logs and supervisor container logs | diff --git a/tasks/scripts/gateway-docker.sh b/tasks/scripts/gateway-docker.sh index c7e71abfed..5a951eb28d 100644 --- a/tasks/scripts/gateway-docker.sh +++ b/tasks/scripts/gateway-docker.sh @@ -18,6 +18,7 @@ # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:docker # OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/... mise run gateway:docker # OPENSHELL_SANDBOX_RUNTIME_IMAGE=ghcr.io/... mise run gateway:docker +# OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317 mise run gateway:docker # # After the gateway is running, point the CLI at it with either: # openshell --gateway docker-dev @@ -83,27 +84,6 @@ ensure_docker_runtime_image() { fi } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - register_gateway_metadata() { local name=$1 local endpoint=$2 @@ -231,7 +211,7 @@ if [[ -n "${OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET+x}" ]]; then printf 'provider_spiffe_workload_api_socket = "%s"\n' "$(toml_escape "${OPENSHELL_PROVIDER_SPIFFE_WORKLOAD_API_SOCKET}")" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-podman.sh b/tasks/scripts/gateway-podman.sh index c032cdad80..bb837e464a 100644 --- a/tasks/scripts/gateway-podman.sh +++ b/tasks/scripts/gateway-podman.sh @@ -19,6 +19,7 @@ # OPENSHELL_SANDBOX_IMAGE=ghcr.io/... mise run gateway:podman # OPENSHELL_SUPERVISOR_IMAGE=ghcr.io/... mise run gateway:podman # OPENSHELL_SANDBOX_RUNTIME_IMAGE=ghcr.io/... mise run gateway:podman +# OPENSHELL_DEV_OTLP_ENDPOINT=http://127.0.0.1:4317 mise run gateway:podman set -euo pipefail @@ -112,27 +113,6 @@ port_is_in_use() { (echo >/dev/tcp/127.0.0.1/"${port}") >/dev/null 2>&1 } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - register_gateway_metadata() { local name=$1 local endpoint=$2 @@ -273,7 +253,7 @@ if [[ -n "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE+x}" ]]; then printf 'proxy_ca_bundle = "%s"\n' "$(toml_escape "${OPENSHELL_SANDBOX_PROXY_CA_BUNDLE}")" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://${CLI_ENDPOINT_HOST}:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" diff --git a/tasks/scripts/gateway-toml.sh b/tasks/scripts/gateway-toml.sh index f9b1e1916b..ce53b639a9 100644 --- a/tasks/scripts/gateway-toml.sh +++ b/tasks/scripts/gateway-toml.sh @@ -17,3 +17,27 @@ toml_escape() { value=${value//$'\t'/\\t} printf '%s' "${value}" } + +# Append OTLP export to a gateway config when OPENSHELL_DEV_OTLP_ENDPOINT is +# set. Export stays off otherwise so no collector is assumed. +append_dev_otlp_config() { + local config_path=$1 + local endpoint=${OPENSHELL_DEV_OTLP_ENDPOINT:-} + if [[ -z "${endpoint}" ]]; then + echo "OTLP export disabled; set OPENSHELL_DEV_OTLP_ENDPOINT to enable it." + return + fi + + cat >>"${config_path}" </dev/tcp/127.0.0.1/"${port}") >/dev/null 2>&1 } -append_local_otlp_config_if_available() { - local config_path=$1 - if ! port_is_in_use 4317; then - echo "OTLP collector not detected on 127.0.0.1:4317; trace export, OCSF log export, and the agent trace relay disabled." - return - fi - - cat >>"${config_path}" <<'EOF' - -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true -EOF - echo "OTLP trace export, OCSF log export, and the agent trace relay enabled for http://127.0.0.1:4317." -} - invoking_user() { if [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then printf '%s\n' "${SUDO_USER}" @@ -397,7 +377,7 @@ if [[ -n "${OPENSHELL_PROVIDER_SPIFFE_ALLOW_GUEST_TCP+x}" ]]; then printf 'provider_spiffe_allow_guest_tcp = %s\n' "${OPENSHELL_PROVIDER_SPIFFE_ALLOW_GUEST_TCP}" >>"${CONFIG_PATH}" fi -append_local_otlp_config_if_available "${CONFIG_PATH}" +append_dev_otlp_config "${CONFIG_PATH}" GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" "${VM_DRIVER_STATE_DIR}" diff --git a/tasks/scripts/gateway.sh b/tasks/scripts/gateway.sh index 3b04ee2684..1fc9d9e9f8 100644 --- a/tasks/scripts/gateway.sh +++ b/tasks/scripts/gateway.sh @@ -18,6 +18,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck source=tasks/scripts/gateway-pull-policy.sh source "${ROOT}/tasks/scripts/gateway-pull-policy.sh" +# shellcheck source=tasks/scripts/gateway-toml.sh +source "${ROOT}/tasks/scripts/gateway-toml.sh" GATEWAY_BIN="${OPENSHELL_GATEWAY_BIN:-${ROOT}/target/debug/openshell-gateway}" usage() { @@ -41,6 +43,9 @@ Environment: or ::1 for Podman Machine on macOS. OPENSHELL_SERVER_PORT Gateway port. Defaults to 8080 for Kubernetes, 18080 for Podman/Docker, and 18081 for VM. + OPENSHELL_DEV_OTLP_ENDPOINT + OTLP collector for trace export, OCSF log export, + and the agent trace relay. Unset disables export. Docker, Podman, and VM runs delegate to their gateway: setup scripts. EOF } @@ -262,15 +267,6 @@ name = "${GATEWAY_NAME}" compute_driver = "${DRIVER}" disable_tls = true -[openshell.gateway.otlp] -endpoint = "http://127.0.0.1:4317" - -[openshell.gateway.otlp.ocsf] -enabled = true - -[openshell.gateway.otlp.agent] -enabled = true - [openshell.gateway.auth] allow_unauthenticated_users = true @@ -293,6 +289,8 @@ if [[ -n "${GRPC_ENDPOINT}" ]]; then printf 'grpc_endpoint = "%s"\n' "${GRPC_ENDPOINT}" >>"${CONFIG_PATH}" fi +append_dev_otlp_config "${CONFIG_PATH}" + GATEWAY_ENDPOINT="http://127.0.0.1:${PORT}" register_gateway_metadata "${GATEWAY_NAME}" "${GATEWAY_ENDPOINT}" "${PORT}" From af8182f7b63da6dd8bbea8b3c313227764ee0dea Mon Sep 17 00:00:00 2001 From: Emilien Macchi Date: Fri, 6 Nov 2026 21:47:53 +0000 Subject: [PATCH 28/31] perf(identity): bound per-connect procfs work in binary identity resolution (#5213) Cut the procfs and hashing work openshell-binary-identity pays for every external connect, all of it synchronous on the sandbox broker thread while the workload's connect(2) stays blocked: - Cap /proc//cmdline reads at 128 KiB so a long command line can no longer grow per-connect work without bound. Reading one byte past the cap keeps the truncation predicate exact, and the truncated tail drops its last argument so no partial fragment reaches cmdline_paths. - Deduplicate the collected cmdline paths with a hash set instead of a quadratic Vec scan, preserving the insertion order consumers compare. - Evict one digest-cache entry at capacity instead of clearing the whole cache, so a capacity crossing no longer forces a full-file rehash of every live executable. - Read /proc//status once per ancestry hop instead of twice, so the NSpid check and the parent PID come from one kernel generation. - Hash executables with a 64 KiB buffer instead of 8 KiB; a same-run release-mode comparison over a page-cached 100 MiB file measures about 1.28x throughput (68.9 ms vs 88.2 ms per hash). Digests, fail-closed Option contracts, and cmdline_paths for untruncated command lines are unchanged; streaming digests stay byte-identical, pinned by an equivalence test against the one-shot facade hash. Closes #5182 Signed-off-by: Emilien Macchi --- crates/openshell-binary-identity/src/lib.rs | 407 +++++++++++++++++--- 1 file changed, 361 insertions(+), 46 deletions(-) diff --git a/crates/openshell-binary-identity/src/lib.rs b/crates/openshell-binary-identity/src/lib.rs index c5dc5696f3..245185bceb 100644 --- a/crates/openshell-binary-identity/src/lib.rs +++ b/crates/openshell-binary-identity/src/lib.rs @@ -13,13 +13,20 @@ use openshell_isolation_interface::contract::{BinaryIdentity, ResolveError}; #[cfg(target_os = "linux")] use openshell_isolation_interface::contract::{ExecutableIdentity, Sha256Digest}; #[cfg(target_os = "linux")] -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[cfg(target_os = "linux")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "linux")] const EXECUTABLE_DIGEST_CACHE_CAPACITY: usize = 1_024; +/// Bounds the total cmdline bytes read from `/proc//cmdline`. This is a +/// diagnostic-read cap on the whole command line, not a per-argument limit: +/// the kernel allows a single argument larger than this cap, so one +/// maximum-size argument plus argv[0] already exceeds it and is truncated. +#[cfg(target_os = "linux")] +const CMDLINE_READ_LIMIT: usize = 128 * 1024; + /// Resolves executable identity from a Linux procfs process identifier. /// /// The configured scope bounds ancestry and cmdline collection to the observed @@ -106,20 +113,13 @@ fn resolve_linux_process( .map(|(snapshot, _)| snapshot.binary_path.clone()) .collect::>(); excluded_paths.push(snapshot.binary_path.clone()); - let cmdline_paths = cmdline_absolute_paths(&snapshot.cmdline) - .into_iter() - .chain( - ancestor_processes - .iter() - .flat_map(|(snapshot, _)| cmdline_absolute_paths(&snapshot.cmdline)), - ) - .filter(|path| !excluded_paths.contains(path)) - .fold(Vec::new(), |mut paths, path| { - if !paths.contains(&path) { - paths.push(path); - } - paths - }); + let cmdline_paths = collect_cmdline_paths( + &snapshot.cmdline, + ancestor_processes + .iter() + .map(|(snapshot, _)| snapshot.cmdline.as_slice()), + &excluded_paths, + ); // Digests computed during this call. They reach the shared cache only after // every snapshot validates, but later processes in the chain reuse them. @@ -243,7 +243,11 @@ fn cache_executable_digest( .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if cache.len() >= EXECUTABLE_DIGEST_CACHE_CAPACITY { - cache.clear(); + // Evict a single arbitrary entry: clearing the whole cache would + // force a rehash of every live executable after one capacity crossing. + if let Some(evict) = cache.keys().next().copied() { + cache.remove(&evict); + } } cache.insert(key, digest); } @@ -335,8 +339,39 @@ fn process_stat(pid: u32) -> Result<(u32, u64), ResolveError> { #[cfg(target_os = "linux")] fn read_process_cmdline(pid: u32) -> Result, ResolveError> { + use std::io::Read as _; + let path = format!("/proc/{pid}/cmdline"); - std::fs::read(&path).map_err(|error| ResolveError::Failed(format!("read {path}: {error}"))) + let file = std::fs::File::open(&path) + // Keep the message prefix std::fs::read produced, so denial-path + // strings downstream stay byte-identical. + .map_err(|error| ResolveError::Failed(format!("read {path}: {error}")))?; + // Reading one byte past the limit makes the truncation predicate exact: + // a cmdline of exactly CMDLINE_READ_LIMIT bytes is kept whole. + let mut buffer = Vec::new(); + file.take(CMDLINE_READ_LIMIT as u64 + 1) + .read_to_end(&mut buffer) + .map_err(|error| ResolveError::Failed(format!("read {path}: {error}")))?; + Ok(cap_cmdline(buffer)) +} + +#[cfg(target_os = "linux")] +fn cap_cmdline(mut buffer: Vec) -> Vec { + if buffer.len() <= CMDLINE_READ_LIMIT { + return buffer; + } + buffer.truncate(CMDLINE_READ_LIMIT); + // Drop the last argument whether it is partial or happens to end exactly + // at the cap: a truncated fragment must not reach cmdline_absolute_paths, + // and diagnostic-only data does not justify boundary-case recovery. + if buffer.last().is_some_and(|byte| *byte == 0) { + buffer.pop(); + } + match buffer.iter().rposition(|byte| *byte == 0) { + Some(index) => buffer.truncate(index + 1), + None => buffer.clear(), + } + buffer } #[cfg(target_os = "linux")] @@ -363,6 +398,11 @@ fn executable_path(pid: u32) -> Result { } #[cfg(target_os = "linux")] +// Stack buffer by design: no heap allocation per cache miss. The array is +// zeroed on each call, a negligible cost next to hashing the executable. +// The 64 KiB size exceeds clippy's 16 KiB large_stack_arrays default, which +// is far below every thread stack in this call path. +#[allow(clippy::large_stack_arrays)] fn hash_executable(pid: u32, executable: &mut std::fs::File) -> Result { use std::fmt::Write as _; use std::io::Read as _; @@ -370,7 +410,7 @@ fn hash_executable(pid: u32, executable: &mut std::fs::File) -> Result Option { - std::fs::read_to_string(format!("/proc/{pid}/status")) - .ok()? - .lines() - .find_map(|line| line.strip_prefix("PPid:"))? - .trim() - .parse() - .ok() +#[derive(Debug, Default, PartialEq, Eq)] +struct ProcessStatus { + ppid: Option, + innermost_nspid: Option, +} + +#[cfg(target_os = "linux")] +fn read_process_status(pid: u32) -> Option { + let path = format!("/proc/{pid}/status"); + Some(parse_process_status(&std::fs::read_to_string(path).ok()?)) +} + +#[cfg(target_os = "linux")] +fn parse_process_status(status: &str) -> ProcessStatus { + let mut parsed = ProcessStatus::default(); + for line in status.lines() { + match line.split_once(':') { + Some(("PPid", value)) => parsed.ppid = value.trim().parse().ok(), + Some(("NSpid", value)) => { + parsed.innermost_nspid = value + .split_whitespace() + .next_back() + .and_then(|nspid| nspid.parse().ok()); + } + _ => {} + } + } + parsed } #[cfg(target_os = "linux")] @@ -440,28 +500,21 @@ fn nested_pid_namespace_init(pid: u32) -> Option { let mut current = pid; for _ in 0..MAX_DEPTH { - if namespace_pid(current) == Some(1) { + // One status read per hop carries both the namespace PID check and + // the parent PID for the next hop. + let status = read_process_status(current)?; + if status.innermost_nspid == Some(1) { // Host PID 1 is outside every workload. A nested namespace init // has a distinct host PID and is a valid workload ancestry root. return (current != 1).then_some(current); } - current = parent_pid(current).filter(|parent| *parent > 0 && *parent != current)?; + current = status + .ppid + .filter(|parent| *parent > 0 && *parent != current)?; } None } -#[cfg(target_os = "linux")] -fn namespace_pid(pid: u32) -> Option { - std::fs::read_to_string(format!("/proc/{pid}/status")) - .ok()? - .lines() - .find_map(|line| line.strip_prefix("NSpid:"))? - .split_whitespace() - .next_back()? - .parse() - .ok() -} - #[cfg(target_os = "linux")] fn cmdline_absolute_paths(cmdline: &[u8]) -> Vec { cmdline @@ -471,6 +524,32 @@ fn cmdline_absolute_paths(cmdline: &[u8]) -> Vec { .collect() } +#[cfg(target_os = "linux")] +fn collect_cmdline_paths<'a>( + own_cmdline: &'a [u8], + ancestor_cmdlines: impl Iterator, + excluded_paths: &[std::path::PathBuf], +) -> Vec { + // Insertion order is load-bearing: consumers compare the collected + // vectors wholesale, so the own cmdline comes first and ancestors follow + // nearest-first. The excluded-paths scan stays a bounded linear check + // over binary paths only. Candidates stream one cmdline at a time, and a + // PathBuf is cloned only for unique survivors, never for duplicates or + // excluded paths. + let mut paths = Vec::new(); + let mut seen_paths: HashSet = HashSet::new(); + for cmdline in std::iter::once(own_cmdline).chain(ancestor_cmdlines) { + for path in cmdline_absolute_paths(cmdline) { + if excluded_paths.contains(&path) || seen_paths.contains(&path) { + continue; + } + seen_paths.insert(path.clone()); + paths.push(path); + } + } + paths +} + #[cfg(all(test, target_os = "linux"))] mod tests { use super::*; @@ -523,15 +602,16 @@ mod tests { let _ = std::io::Read::read(&mut std::io::stdin(), &mut [0_u8]); } - #[test] - fn resolves_child_and_hashes_ancestor_chain() { - let parent_pid = std::process::id(); + /// Spawns `identity_helper_process` with optional extra argv entries and + /// waits for its readiness signal. Only the spawn itself reports errors: + /// callers decide how to react (the long-cmdline test skips on E2BIG). + fn spawn_identity_helper(extra_args: &[String]) -> std::io::Result { let mut child = std::process::Command::new(std::env::current_exe().unwrap()) .args(["--ignored", "--exact", "tests::identity_helper_process"]) + .args(extra_args) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) - .spawn() - .expect("spawn child"); + .spawn()?; let mut child_stdout = std::io::BufReader::new(child.stdout.take().expect("child stdout")); let mut line = String::new(); // Wait until the helper has entered the test before taking the @@ -545,7 +625,13 @@ mod tests { break; } } - let child = ChildGuard(child); + Ok(ChildGuard(child)) + } + + #[test] + fn resolves_child_and_hashes_ancestor_chain() { + let parent_pid = std::process::id(); + let child = spawn_identity_helper(&[]).expect("spawn child"); let identity = ProcfsIdentityResolver::for_process_tree(parent_pid) .resolve(child.0.id()) @@ -579,6 +665,41 @@ mod tests { assert!(cache.lock().unwrap().is_empty()); } + #[test] + fn hash_executable_matches_one_shot_sha256_over_the_same_file() { + let current_exe = std::env::current_exe().unwrap(); + let mut executable = std::fs::File::open(¤t_exe).unwrap(); + + let streamed = hash_executable(std::process::id(), &mut executable).unwrap(); + let one_shot = openshell_crypto::sha256(&std::fs::read(¤t_exe).unwrap()).unwrap(); + + assert_eq!(streamed.as_bytes(), &one_shot); + } + + #[test] + fn cache_executable_digest_evicts_one_entry_at_capacity() { + let sentinel: Sha256Digest = "ab".repeat(32).parse().unwrap(); + let cache = Mutex::new(HashMap::new()); + for index in 0..EXECUTABLE_DIGEST_CACHE_CAPACITY as u64 { + cache_executable_digest(&cache, synthetic_cache_key(index), sentinel); + } + assert_eq!( + cache.lock().unwrap().len(), + EXECUTABLE_DIGEST_CACHE_CAPACITY + ); + + let newest_key = synthetic_cache_key(EXECUTABLE_DIGEST_CACHE_CAPACITY as u64); + let newest_sentinel: Sha256Digest = "cd".repeat(32).parse().unwrap(); + cache_executable_digest(&cache, newest_key, newest_sentinel); + + // Crossing the capacity evicts exactly one entry, not the whole cache. + // Which entry drops is arbitrary, so only the size and the newest + // mapping are asserted. + let cache = cache.lock().unwrap(); + assert_eq!(cache.len(), EXECUTABLE_DIGEST_CACHE_CAPACITY); + assert_eq!(cache.get(&newest_key), Some(&newest_sentinel)); + } + #[test] fn process_tree_root_does_not_escape_into_host_ancestry() { let pid = std::process::id(); @@ -588,4 +709,198 @@ mod tests { assert!(identity.ancestors.is_empty()); } + + fn synthetic_cache_key(index: u64) -> ExecutableCacheKey { + ExecutableCacheKey { + device: 1, + inode: index, + size: 1, + mtime: 0, + mtime_nsec: 0, + ctime: 0, + ctime_nsec: 0, + } + } + + #[test] + fn resolves_child_with_cmdline_beyond_the_read_limit() { + // The kernel caps a single argument at a size that already reaches + // the whole-cmdline cap, so one oversized argument fails spawn with + // E2BIG before the child ever runs. The cmdline is instead pushed + // past CMDLINE_READ_LIMIT with several arguments that each stay under + // the per-argument limit. Separately, the whole argv+env list is + // capped at RLIMIT_STACK/4, so a host with a small stack limit can + // still reject the filler list with E2BIG; that skips the test + // instead of failing it. + let filler = "x".repeat(64 * 1024); + let fillers = [filler.clone(), filler.clone(), filler]; + let parent_pid = std::process::id(); + let child = match spawn_identity_helper(&fillers) { + Ok(child) => child, + Err(error) if error.kind() == std::io::ErrorKind::ArgumentListTooLong => { + eprintln!( + "skipping: host argv+env limit (RLIMIT_STACK/4) too small for the filler arguments" + ); + return; + } + Err(error) => panic!("spawn child: {error}"), + }; + + let identity = ProcfsIdentityResolver::for_process_tree(parent_pid) + .resolve(child.0.id()) + .expect("resolve child identity"); + assert!(identity.executable.digest.is_some()); + + let cmdline = read_process_cmdline(child.0.id()).expect("read capped cmdline"); + assert!(cmdline.len() <= CMDLINE_READ_LIMIT); + // The capped cmdline ends on an argument boundary, not mid-argument. + assert_eq!(cmdline.last(), Some(&0)); + // Every surviving filler argument is complete: the argv prefix + // (executable path plus harness flags) is far below one filler, so + // the cut lands inside the second filler and only the first one, + // which ends before the cap, survives whole. + let complete_fillers = cmdline + .split(|byte| *byte == 0) + .filter(|argument| !argument.is_empty() && argument.iter().all(|byte| *byte == b'x')) + .count(); + assert_eq!(complete_fillers, 1); + } + + #[test] + fn cap_cmdline_keeps_cmdline_at_the_limit_whole() { + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + assert_eq!(buffer.len(), CMDLINE_READ_LIMIT); + + assert_eq!(cap_cmdline(buffer.clone()), buffer); + } + + #[test] + fn cap_cmdline_truncates_over_limit_cmdline_and_drops_last_argument() { + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + buffer.push(b'x'); + assert_eq!(buffer.len(), CMDLINE_READ_LIMIT + 1); + + assert_eq!(cap_cmdline(buffer), b"/bin/one\0"); + } + + #[test] + fn cap_cmdline_drops_argument_terminated_at_the_cut_point() { + // The NUL terminating the second argument is the last byte of the + // cut region: the argument is complete, but the cap deliberately + // drops it rather than recover boundary-case arguments. + let mut buffer = b"/bin/one\0".to_vec(); + buffer.extend(vec![b'a'; CMDLINE_READ_LIMIT - buffer.len() - 1]); + buffer.push(0); + buffer.extend(b"/bin/two\0"); + assert!(buffer.len() > CMDLINE_READ_LIMIT); + + assert_eq!(cap_cmdline(buffer), b"/bin/one\0"); + } + + #[test] + fn cap_cmdline_drops_sole_argument_over_the_limit_entirely() { + let buffer = vec![b'a'; CMDLINE_READ_LIMIT + 1]; + + assert_eq!(cap_cmdline(buffer), Vec::::new()); + } + + #[test] + fn cap_cmdline_passes_short_cmdlines_through() { + let without_trailing_nul = b"/bin/one\0/bin/two".to_vec(); + assert_eq!( + cap_cmdline(without_trailing_nul.clone()), + without_trailing_nul + ); + + assert_eq!(cap_cmdline(Vec::new()), Vec::::new()); + } + + #[test] + fn read_process_cmdline_maps_a_missing_process_to_a_read_error() { + // u32::MAX - 1 is beyond the kernel PID ceiling, so reading + // /proc//cmdline fails. The message prefix must match what + // std::fs::read produced, since the string reaches identity-source + // denial paths downstream. + let pid = u32::MAX - 1; + match read_process_cmdline(pid) { + Err(ResolveError::Failed(message)) => { + let expected = format!("read /proc/{pid}/cmdline: "); + assert!( + message.starts_with(&expected), + "unexpected error message: {message}" + ); + } + other => panic!("expected a failed cmdline read, got {other:?}"), + } + } + + #[test] + fn parse_process_status_reads_ppid_and_innermost_nspid() { + let status = "Name:\tbash\nUmask:\t0022\nState:\tS\nPPid:\t1234\n\ + NSpid:\t5678\t9\n"; + let parsed = parse_process_status(status); + assert_eq!(parsed.ppid, Some(1234)); + // The last NSpid field is the innermost namespace PID. + assert_eq!(parsed.innermost_nspid, Some(9)); + } + + #[test] + fn parse_process_status_keeps_a_zero_ppid_distinct_from_a_missing_one() { + // Some(0) vs None is load-bearing: the ancestry walk's `parent > 0` + // termination filter relies on a present-but-zero PPid being Some. + let parsed = parse_process_status("Name:\tkthreadd\nPPid:\t0\nNSpid:\t2\n"); + assert_eq!(parsed.ppid, Some(0)); + assert_eq!(parsed.innermost_nspid, Some(2)); + } + + #[test] + fn parse_process_status_reports_missing_fields_as_none() { + let parsed = parse_process_status("Name:\tbash\nState:\tS\n"); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + } + + #[test] + fn parse_process_status_reports_garbage_as_none() { + let parsed = parse_process_status(""); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + + let parsed = parse_process_status("PPid:\tnot-a-pid\nNSpid:\talso not pids\n"); + assert_eq!(parsed.ppid, None); + assert_eq!(parsed.innermost_nspid, None); + } + + #[test] + fn read_process_status_returns_none_for_a_missing_process() { + // u32::MAX - 1 is beyond the kernel PID ceiling, so the status file + // cannot be read and the namespace walk terminates on this hop. + // parse_process_status alone never yields None: only the failed read + // does, which keeps a garbage status distinct from an absent one. + assert_eq!(read_process_status(u32::MAX - 1), None); + } + + #[test] + fn collect_cmdline_paths_preserves_order_and_deduplicates() { + let own = b"/own/exe\0/usr/bin/dupe\0relative\0".to_vec(); + let ancestors = [ + b"/anc/one\0/usr/bin/dupe\0".to_vec(), + b"/anc/one\0/anc/two\0".to_vec(), + ]; + let excluded_paths = vec![std::path::PathBuf::from("/own/exe")]; + + let paths = + collect_cmdline_paths(&own, ancestors.iter().map(Vec::as_slice), &excluded_paths); + + let expected = [ + std::path::PathBuf::from("/usr/bin/dupe"), + std::path::PathBuf::from("/anc/one"), + std::path::PathBuf::from("/anc/two"), + ]; + assert_eq!(paths, expected); + } } From af020160f5537a7f08ea9c26e015f6c2302cdc2f Mon Sep 17 00:00:00 2001 From: Eric Curtin Date: Sun, 8 Nov 2026 22:29:50 +0000 Subject: [PATCH 29/31] fix(cli): accept allow-encoded-slash in policy update (#4968) Also merge the two protocol check helpers into one. Signed-off-by: Eric Curtin --- crates/openshell-cli/src/main.rs | 4 +- crates/openshell-cli/src/policy_update.rs | 80 ++++++++++++++------ docs/how-it-works/policies/network-rules.mdx | 4 + 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/crates/openshell-cli/src/main.rs b/crates/openshell-cli/src/main.rs index 7411629483..18297f1518 100644 --- a/crates/openshell-cli/src/main.rs +++ b/crates/openshell-cli/src/main.rs @@ -2191,8 +2191,8 @@ enum PolicyCommands { name: Option, /// Add or merge an endpoint: host:port[:access[:protocol[:enforcement[:options]]]]. - /// Options include allowed-ip=..., credential rewrite flags, and - /// allow-uninspected-credentials. + /// Options include allowed-ip=..., allow-encoded-slash, credential + /// rewrite flags, and allow-uninspected-credentials. #[arg(long = "add-endpoint")] add_endpoints: Vec, diff --git a/crates/openshell-cli/src/policy_update.rs b/crates/openshell-cli/src/policy_update.rs index 2907afaaed..6aac515e14 100644 --- a/crates/openshell-cli/src/policy_update.rs +++ b/crates/openshell-cli/src/policy_update.rs @@ -268,11 +268,14 @@ fn l7_target_to_proto(target: &L7RuleTarget) -> ProtoL7RuleTarget { } } -fn ensure_websocket_credential_rewrite_protocol( +/// Require the endpoint protocol segment to be one of `allowed` for `option`. +fn ensure_option_protocol( spec: &str, endpoint: &NetworkEndpoint, + option: &str, + allowed: &[&str], ) -> Result<()> { - if matches!(endpoint.protocol.as_str(), "rest" | "websocket") { + if allowed.contains(&endpoint.protocol.as_str()) { return Ok(()); } let protocol = if endpoint.protocol.is_empty() { @@ -280,25 +283,13 @@ fn ensure_websocket_credential_rewrite_protocol( } else { endpoint.protocol.as_str() }; + let allowed = allowed + .iter() + .map(|protocol| format!("'{protocol}'")) + .collect::>() + .join(" or "); Err(miette!( - "websocket-credential-rewrite endpoint option requires --add-endpoint protocol segment to be 'rest' or 'websocket'; got '{protocol}' in '{spec}'" - )) -} - -fn ensure_request_body_credential_rewrite_protocol( - spec: &str, - endpoint: &NetworkEndpoint, -) -> Result<()> { - if endpoint.protocol == "rest" { - return Ok(()); - } - let protocol = if endpoint.protocol.is_empty() { - "" - } else { - endpoint.protocol.as_str() - }; - Err(miette!( - "request-body-credential-rewrite endpoint option requires --add-endpoint protocol segment to be 'rest'; got '{protocol}' in '{spec}'" + "{option} endpoint option requires --add-endpoint protocol segment to be {allowed}; got '{protocol}' in '{spec}'" )) } @@ -504,12 +495,16 @@ fn apply_add_endpoint_options( "allow-uninspected-credentials" => { endpoint.allow_uninspected_credentials = true; } + "allow-encoded-slash" => { + ensure_option_protocol(spec, endpoint, option, &["rest", "websocket"])?; + endpoint.allow_encoded_slash = true; + } "websocket-credential-rewrite" => { - ensure_websocket_credential_rewrite_protocol(spec, endpoint)?; + ensure_option_protocol(spec, endpoint, option, &["rest", "websocket"])?; endpoint.websocket_credential_rewrite = true; } "request-body-credential-rewrite" => { - ensure_request_body_credential_rewrite_protocol(spec, endpoint)?; + ensure_option_protocol(spec, endpoint, option, &["rest"])?; endpoint.request_body_credential_rewrite = true; } _ if option.starts_with(ALLOWED_IP_OPTION_PREFIX) => { @@ -521,7 +516,7 @@ fn apply_add_endpoint_options( } _ => { return Err(miette!( - "--add-endpoint options segment supports only 'allow-uninspected-credentials', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip='; got '{option}' in '{spec}'" + "--add-endpoint options segment supports only 'allow-uninspected-credentials', 'allow-encoded-slash', 'websocket-credential-rewrite', 'request-body-credential-rewrite', and 'allowed-ip='; got '{option}' in '{spec}'" )); } } @@ -831,6 +826,45 @@ mod tests { assert!(endpoint.request_body_credential_rewrite); } + #[test] + fn parse_add_endpoint_enables_allow_encoded_slash_on_rest_and_websocket() { + for protocol in ["rest", "websocket"] { + let plan = build_policy_update_plan( + &[format!( + "registry.npmjs.org:443:full:{protocol}:enforce:allow-encoded-slash" + )], + &[], + &[], + &[], + &[], + &[], + None, + ) + .expect("plan should build"); + + let PolicyMergeOp::AddRule { rule, .. } = &plan.preview_operations[0] else { + panic!("expected add-rule preview"); + }; + assert!(rule.endpoints[0].allow_encoded_slash, "{protocol}"); + } + } + + #[test] + fn allow_encoded_slash_rejects_endpoint_without_http_protocol() { + for spec in [ + "registry.npmjs.org:443::::allow-encoded-slash", + "registry.npmjs.org:443::tcp::allow-encoded-slash", + ] { + let error = + build_policy_update_plan(&[spec.to_string()], &[], &[], &[], &[], &[], None) + .expect_err("plan should fail"); + assert!( + error.to_string().contains("to be 'rest' or 'websocket'"), + "{error}" + ); + } + } + #[test] fn parse_add_endpoint_enables_allow_uninspected_credentials() { let plan = build_policy_update_plan( diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index d7dec792a6..119818a395 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -343,6 +343,10 @@ request, which the read-only preset denies. Run `npm install --no-audit`, or replace `access: read-only` with explicit rules that also allow the audit's `POST` requests. An endpoint cannot combine `access` and `rules`. +To set the option on a live sandbox without replacing the policy, add +`allow-encoded-slash` to the `--add-endpoint` options segment of +`openshell policy update`, which requires a `rest` or `websocket` endpoint. + Verify that a scoped package lookup succeeds: ```shell From 45cd02822453ac684a2923f86b0e7a8a17f6f50f Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Sun, 8 Nov 2026 23:36:36 +0000 Subject: [PATCH 30/31] fix(server): deliver events published while a watch opens Signed-off-by: Artem Lytvyn --- crates/openshell-server/src/grpc/sandbox.rs | 273 +++++++-- crates/openshell-server/src/sandbox_watch.rs | 62 ++ crates/openshell-server/src/tracing_bus.rs | 581 +++++++++++++++---- docs/observability/accessing-logs.mdx | 2 + proto/openshell.proto | 5 +- 5 files changed, 765 insertions(+), 158 deletions(-) diff --git a/crates/openshell-server/src/grpc/sandbox.rs b/crates/openshell-server/src/grpc/sandbox.rs index 3bd34b449b..6e7dea93f9 100644 --- a/crates/openshell-server/src/grpc/sandbox.rs +++ b/crates/openshell-server/src/grpc/sandbox.rs @@ -1895,21 +1895,19 @@ pub(super) async fn handle_watch_sandbox( } else { None }; - let mut log_rx = if follow_logs { - Some(state.tracing_log_bus.subscribe(&sandbox_id)) - } else { - None - }; - let mut platform_rx = if follow_events { - Some( - state - .tracing_log_bus - .platform_event_bus - .subscribe(&sandbox_id), - ) - } else { - None - }; + // + // The resumable sources subscribe together with a high-water mark + // taken under the cursor-space lock, so the snapshot below can + // tell history (at or below the mark, buffered only) from events + // published while the stream opens (above it, buffered *and* + // queued on these receivers). + let crate::tracing_bus::ResumableSubscription { + mut log_rx, + mut platform_rx, + subscribed_at, + } = state + .tracing_log_bus + .subscribe_resumable(&sandbox_id, follow_logs, follow_events); // Re-read the snapshot now that we have subscriptions active. match state.store.get_message::(&sandbox_id).await { @@ -1949,26 +1947,26 @@ pub(super) async fn handle_watch_sandbox( // Highest seq the tail/replay phase already handled, tracked per // source. The broadcast receivers were subscribed before replay ran, - // so an event published during initialization can sit in both the - // replay buffer and a live receiver; the live loop suppresses events - // at or below its source's mark so each is delivered exactly once. + // so an event published during initialization (above + // `subscribed_at`) sits in both the replay buffer and a live + // receiver; the live loop suppresses events at or below its + // source's mark so each is delivered exactly once. // // Both reads happen under one cursor-space lock hold, so every // event at or below the snapshot's high-water mark was buffered // when they ran. A resume replays everything after the cursor from // each followed source, so each source's mark is the last seq its // own replay returned. The initial tail is depth-bounded, so both - // marks move to the snapshot's high-water mark instead: anything at - // or below it was either in the batch or deliberately left out as - // pre-snapshot history, and its live copy must not arrive after a - // higher cursor from the batch. + // marks move to the snapshot's high-water mark instead. That is + // safe only because the depth bound never applies above + // `subscribed_at`: every event published while the stream opened + // is in the batch (or, if its buffer already evicted it, reported), + // and everything else at or below the mark is history the live + // receivers never saw. // - // No gateway-level test reaches the window between the subscribe - // above and the snapshot below: the producer crosses it with no - // await a test can wedge open. The bus-level - // `snapshot_tail_blocks_while_cursor_space_is_locked` and - // `snapshot_after_blocks_while_cursor_space_is_locked` pin the - // atomicity this relies on. + // Tests force both windows through the bus's test-only snapshot + // hooks: one publishes after subscribe and before the snapshot, the + // other publishes between the snapshot's two reads. let resume_seq = resume_after.map_or(0, |resume| resume.seq); let mut log_cutoff: u64 = resume_seq; let mut platform_cutoff: u64 = resume_seq; @@ -2126,17 +2124,32 @@ pub(super) async fn handle_watch_sandbox( // log published after the log read and a platform event // published after it could let the platform window hand out // the higher cursor while the log arrives live afterwards. + // + // Depths bound history only. Events published after the + // receivers subscribed are always in their source's window, so + // the batch can exceed `log_tail_lines`/`event_tail` by them. + // Each floor therefore stays at or below `subscribed_at`, and + // none of those events is ever withheld by a sibling's floor. let crate::tracing_bus::TailSnapshot { - mut logs, - log_floor, - mut events, - platform_floor, + log, + platform, highest_seq, } = state.tracing_log_bus.snapshot_tail( &sandbox_id, follow_logs.then_some(log_tail as usize), follow_events.then_some(event_tail as usize), + subscribed_at, ); + let crate::tracing_bus::SourceTail { + events: mut logs, + floor: log_floor, + evicted_while_opening: logs_evicted, + } = log; + let crate::tracing_bus::SourceTail { + mut events, + floor: platform_floor, + evicted_while_opening: events_evicted, + } = platform; // 0 means "nothing excluded" for a source, so a fully-covered // source never raises the maximum. @@ -2151,11 +2164,12 @@ pub(super) async fn handle_watch_sandbox( // Both cutoffs move to the snapshot's high-water mark. The // receivers were subscribed before the snapshot, so an event - // published in between is both buffered and queued live. At - // or below the mark, it was either delivered above or left - // out of this batch as pre-snapshot history; delivering its - // live copy would emit a lower cursor after a higher one - // from the batch. Above the mark, it can only arrive live. + // published in between is both buffered and queued live; the + // snapshot kept it in the batch, so its live copy is a + // duplicate, and delivering it would emit a lower cursor after + // a higher one from the batch. Events at or below + // `subscribed_at` were never queued live. Above the mark, an + // event can only arrive live. log_cutoff = log_cutoff.max(highest_seq); platform_cutoff = platform_cutoff.max(highest_seq); @@ -2183,6 +2197,18 @@ pub(super) async fn handle_watch_sandbox( return; } } + // An event published while the stream opened, then trimmed + // from its buffer before the snapshot read it, is in neither + // the batch nor (past the cutoff) the live stream. + if logs_evicted || events_evicted { + let warning = crate::sandbox_watch::opening_eviction_warning_event( + logs_evicted, + events_evicted, + ); + if tx.send(Ok(warning)).await.is_err() { + return; + } + } for cursored in tail { // Log filters; platform events carry no log fields and pass. @@ -5127,6 +5153,179 @@ mod tests { assert_eq!(delivered, vec![3, 4]); } + /// Open a watch following both sources and return the stream after its + /// status snapshot. + async fn watch_both( + state: &Arc, + sandbox: &Sandbox, + event_tail: u32, + resume_after_cursor: String, + ) -> WatchSandboxStream { + use tokio_stream::StreamExt as _; + + let response = handle_watch_sandbox( + state, + authed_request(WatchSandboxRequest { + sandbox: sandbox.object_name().to_string(), + workspace_scope: Some(openshell_core::proto::workspace_selector("default")), + follow_logs: true, + follow_events: true, + event_tail, + resume_after_cursor, + ..Default::default() + }), + ) + .await + .unwrap(); + let mut stream = response.into_inner(); + let snap = stream.next().await.unwrap().unwrap(); + assert!(snap.cursor.is_empty(), "first item is the status snapshot"); + stream + } + + /// Collect what a stream yields until it goes quiet: warning messages and + /// resumable cursors, each in arrival order. + async fn drain_until_quiet(stream: &mut WatchSandboxStream) -> (Vec, Vec) { + use openshell_core::proto::sandbox_stream_event::Payload; + use tokio_stream::StreamExt as _; + + let mut warnings = Vec::new(); + let mut seqs = Vec::new(); + while let Ok(Some(item)) = + tokio::time::timeout(std::time::Duration::from_millis(200), stream.next()).await + { + let event = item.unwrap(); + match event.payload { + Some(Payload::Warning(w)) => warnings.push(w.message), + _ => seqs.push(seq_of(&event)), + } + } + (warnings, seqs) + } + + /// The reviewer's example on #3613. Both sources followed at default + /// depths; after the watch subscribes and before its snapshot, platform + /// event 1 and then log 2 are published. Both are queued live and the + /// snapshot's cutoff suppresses those copies, so the batch is their only + /// delivery: it must carry both, even though `event_tail` asked for no + /// platform history. Previously the depth bound left event 1 out and the + /// cutoff swallowed its live copy, with no warning, and a resume after 2 + /// could never recover it. + #[tokio::test] + async fn initial_tail_delivers_events_published_while_the_stream_opens() { + let state = test_server_state().await; + let sandbox = test_sandbox("opening", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + let hook_state = Arc::clone(&state); + let hook_id = id.clone(); + let mut fired = false; + state.tracing_log_bus.set_snapshot_hook(move |point| { + if point == crate::tracing_bus::SnapshotPoint::AfterSubscribe && !fired { + fired = true; + seed_platform_event(&hook_state, &hook_id, "e1"); // cursor 1 + seed_log_lines(&hook_state, &hook_id, 1); // cursor 2 + } + }); + + let mut stream = watch_both(&state, &sandbox, 0, String::new()).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + assert_eq!(delivered, vec![1, 2]); + assert!(warnings.is_empty(), "nothing was withheld: {warnings:?}"); + + // Live delivery continues past the batch. + seed_log_lines(&state, &id, 1); // cursor 3 + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + assert_eq!(delivered, vec![3]); + assert!(warnings.is_empty()); + + // A reconnect from the highest cursor the batch handed out misses + // nothing that was retained below it. + let mut resumed = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert_eq!(delivered, vec![3]); + assert!(warnings.is_empty()); + } + + /// A publish forced between the initial snapshot's log read and platform + /// read. The snapshot holds the cursor-space lock across both, so the + /// publisher waits: the batch is exactly the seeded history, the raced + /// log and platform event arrive live in cursor order once each, and a + /// resume from the batch's highest cursor replays both. With separate + /// reads the batch would carry the raced platform event (4) without the + /// log published before it (3), which would then arrive after 4 or, after + /// a reconnect from 4, never. + #[tokio::test] + async fn initial_tail_publish_between_snapshot_reads_arrives_live_in_order() { + let state = test_server_state().await; + let sandbox = test_sandbox("racetail", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + let probe = crate::tracing_bus::race_publish_between_reads(&state.tracing_log_bus, &id); + + let mut stream = watch_both(&state, &sandbox, 10, String::new()).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } + assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}"); + assert_eq!(delivered, vec![1, 2, 3, 4], "batch, then 3 and 4 live"); + + let mut resumed = watch_both(&state, &sandbox, 10, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert!(warnings.is_empty()); + assert_eq!(delivered, vec![3, 4]); + } + + /// The same forced interleaving on the resume path. The replay after + /// cursor 1 holds platform event 2 only; the raced log (3) and platform + /// event (4) arrive live, in order, after it. With separate reads the + /// replay would carry 4 ahead of 3, and a client that disconnected after 4 + /// would resume past 3 forever. + #[tokio::test] + async fn resume_publish_between_snapshot_reads_arrives_live_in_order() { + let state = test_server_state().await; + let sandbox = test_sandbox("raceresume", Vec::new()); + state.store.put_message(&sandbox).await.unwrap(); + let id = sandbox.object_id().to_string(); + + seed_log_lines(&state, &id, 1); // cursor 1 + seed_platform_event(&state, &id, "e2"); // cursor 2 + let probe = crate::tracing_bus::race_publish_between_reads(&state.tracing_log_bus, &id); + + let mut stream = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 1)).await; + let (warnings, delivered) = drain_until_quiet(&mut stream).await; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } + assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}"); + assert_eq!(delivered, vec![2, 3, 4], "replay, then 3 and 4 live"); + + // Reconnecting from the replay's only cursor recovers both. + let mut resumed = watch_both(&state, &sandbox, 0, cursor_token(&state, &id, 2)).await; + let (warnings, delivered) = drain_until_quiet(&mut resumed).await; + assert!(warnings.is_empty()); + assert_eq!(delivered, vec![3, 4]); + } + #[tokio::test] async fn live_delivery_orders_events_across_sources_by_cursor() { use tokio_stream::StreamExt as _; diff --git a/crates/openshell-server/src/sandbox_watch.rs b/crates/openshell-server/src/sandbox_watch.rs index 7bc3ce8728..c79c81d399 100644 --- a/crates/openshell-server/src/sandbox_watch.rs +++ b/crates/openshell-server/src/sandbox_watch.rs @@ -191,6 +191,44 @@ pub fn coverage_gap_warning_event( } } +/// Build the warning payload emitted when a bus evicted events published +/// while the watch was opening, before the initial snapshot could read them. +/// +/// Those events were queued on the live receivers too, but the snapshot's +/// cutoff suppresses live copies at or below its high-water mark, so they are +/// in neither the batch nor the live stream. The bus cannot say how many were +/// lost -- its seqs interleave with the sibling source's -- only which source. +pub fn opening_eviction_warning(logs: bool, platform_events: bool) -> SandboxStreamWarning { + let what = match (logs, platform_events) { + (true, true) => "log lines and platform events", + (true, false) => "log lines", + _ => "platform events", + }; + SandboxStreamWarning { + message: format!( + "some {what} published while this watch was opening were evicted from the \ + gateway buffer before they could be sent; they are not recoverable from any \ + cursor this stream hands out." + ), + } +} + +/// Wrap [`opening_eviction_warning`] in a `SandboxStreamEvent` ready to send. +pub fn opening_eviction_warning_event( + logs: bool, + platform_events: bool, +) -> openshell_core::proto::SandboxStreamEvent { + use openshell_core::proto::sandbox_stream_event::Payload; + openshell_core::proto::SandboxStreamEvent { + payload: Some(Payload::Warning(opening_eviction_warning( + logs, + platform_events, + ))), + // Warnings are not part of the resumable log/platform sequence. + cursor: String::new(), + } +} + #[cfg(test)] mod tests { use super::*; @@ -278,6 +316,30 @@ mod tests { ); } + #[test] + fn opening_eviction_warning_names_the_affected_sources() { + assert!( + opening_eviction_warning(true, false) + .message + .contains("some log lines ") + ); + assert!( + opening_eviction_warning(false, true) + .message + .contains("some platform events ") + ); + assert!( + opening_eviction_warning(true, true) + .message + .contains("log lines and platform events") + ); + assert!( + opening_eviction_warning_event(true, false) + .cursor + .is_empty() + ); + } + #[test] fn coverage_gap_warning_event_wraps_warning_payload_with_empty_cursor() { use openshell_core::proto::sandbox_stream_event::Payload; diff --git a/crates/openshell-server/src/tracing_bus.rs b/crates/openshell-server/src/tracing_bus.rs index ef8cc22874..5677f3cb49 100644 --- a/crates/openshell-server/src/tracing_bus.rs +++ b/crates/openshell-server/src/tracing_bus.rs @@ -22,6 +22,8 @@ pub struct TracingLogBus { inner: Arc>, pub(crate) platform_event_bus: PlatformEventBus, seq: SeqAllocator, + #[cfg(test)] + hooks: SnapshotHooks, } #[derive(Debug, Clone)] @@ -206,8 +208,22 @@ fn tail_after_impl( /// resume from," which `tail_after`'s eviction-only gap check can't: a /// shallow request and a bus eviction both withhold events, but only the /// eviction leaves a trace in `last_trimmed_seq`. -fn tail_with_floor_impl(tail: &VecDeque, max: usize) -> (Vec, u64) { - let excluded = tail.len().saturating_sub(max); +/// +/// `keep_after` exempts the newest events from `max`: every retained event +/// with a seq above it is returned regardless of depth. The watch handler +/// passes the high-water mark it recorded when it subscribed, so an event +/// published while the stream was opening -- one that is also queued on the +/// live receiver, where the snapshot's cutoff will suppress it -- is always +/// delivered in the batch rather than trimmed as history. The returned set is +/// still a suffix of the tail, so the floor keeps its meaning, and it never +/// exceeds `keep_after`. +fn tail_with_floor_impl( + tail: &VecDeque, + max: usize, + keep_after: u64, +) -> (Vec, u64) { + let first_unseen = tail.partition_point(|cursored| cursored.seq <= keep_after); + let excluded = tail.len().saturating_sub(max).min(first_unseen); let floor = excluded .checked_sub(1) .and_then(|newest_excluded| tail.get(newest_excluded)) @@ -234,6 +250,8 @@ impl TracingLogBus { })), platform_event_bus: PlatformEventBus::new(seq.clone()), seq, + #[cfg(test)] + hooks: SnapshotHooks::default(), } } @@ -397,6 +415,8 @@ impl TracingLogBus { |per| tail_after_impl(&per.tail, per.last_trimmed_seq, log_after), ) }); + #[cfg(test)] + self.hooks.run(SnapshotPoint::BetweenReads); let platform = want_platform.then(|| { let inner = self .platform_event_bus @@ -422,6 +442,15 @@ impl TracingLogBus { /// tails were read, so the caller can suppress live copies at or below it /// without dropping anything the tails could not have covered. /// + /// `subscribed_at` is the mark [`Self::subscribe_resumable`] recorded. + /// Events above it were published after the caller's live receivers + /// existed, so they are queued live as well as buffered here; the caller + /// suppresses those live copies, which means this batch is their only + /// delivery. Each source therefore returns all of them regardless of its + /// depth (see `tail_with_floor_impl`), and reports whether its buffer + /// already evicted any of them before the read, in which case they are + /// lost from both paths and the caller must say so. + /// /// Reads never mint a space: with none, both tails are empty and /// `highest_seq` is `0`. pub(crate) fn snapshot_tail( @@ -429,56 +458,260 @@ impl TracingLogBus { sandbox_id: &str, log_max: Option, platform_max: Option, + subscribed_at: u64, ) -> TailSnapshot { let spaces = self.seq.lock(); let highest_seq = spaces .get(sandbox_id) .map_or(0, |space| space.next.saturating_sub(1)); - let (logs, log_floor) = log_max.map_or_else( - || (Vec::new(), 0), - |max| { - let inner = self.inner.lock().expect("tracing bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || (Vec::new(), 0), - |per| tail_with_floor_impl(&per.tail, max), - ) - }, - ); - let (events, platform_floor) = platform_max.map_or_else( - || (Vec::new(), 0), - |max| { - let inner = self - .platform_event_bus - .inner - .lock() - .expect("platform event bus lock poisoned"); - inner.per_id.get(sandbox_id).map_or_else( - || (Vec::new(), 0), - |per| tail_with_floor_impl(&per.tail, max), - ) - }, - ); + let read = |per: Option<&PerSandbox>, max: usize| { + per.map_or_else(SourceTail::default, |per| { + let (events, floor) = tail_with_floor_impl(&per.tail, max, subscribed_at); + SourceTail { + events, + floor, + evicted_while_opening: per.last_trimmed_seq > subscribed_at, + } + }) + }; + + let log = log_max.map_or_else(SourceTail::default, |max| { + let inner = self.inner.lock().expect("tracing bus lock poisoned"); + read(inner.per_id.get(sandbox_id), max) + }); + #[cfg(test)] + self.hooks.run(SnapshotPoint::BetweenReads); + let platform = platform_max.map_or_else(SourceTail::default, |max| { + let inner = self + .platform_event_bus + .inner + .lock() + .expect("platform event bus lock poisoned"); + read(inner.per_id.get(sandbox_id), max) + }); drop(spaces); TailSnapshot { - logs, - log_floor, - events, - platform_floor, + log, + platform, highest_seq, } } + + /// Subscribe live receivers for the resumable sources and record the + /// space's high-water mark, all under one cursor-space lock hold. + /// + /// Every publish holds this lock across its broadcast send, so the mark + /// splits the space exactly: an event at or below it was sent before + /// these receivers existed and can only be found in a buffer, and one + /// above it is queued on them. [`Self::snapshot_tail`] uses the mark to + /// keep every queued-and-buffered event in the initial batch. + /// + /// The lock order is allocator -> bus map, the same as publish. Never + /// mints a space: with none, the mark is `0`. + pub(crate) fn subscribe_resumable( + &self, + sandbox_id: &str, + want_log: bool, + want_platform: bool, + ) -> ResumableSubscription { + let spaces = self.seq.lock(); + let subscription = ResumableSubscription { + log_rx: want_log.then(|| self.subscribe(sandbox_id)), + platform_rx: want_platform.then(|| self.platform_event_bus.subscribe(sandbox_id)), + subscribed_at: spaces + .get(sandbox_id) + .map_or(0, |space| space.next.saturating_sub(1)), + }; + drop(spaces); + #[cfg(test)] + self.hooks.run(SnapshotPoint::AfterSubscribe); + subscription + } + + /// Install a hook that runs at each [`SnapshotPoint`], replacing any + /// previous one. Shared by every clone of this bus. + #[cfg(test)] + pub(crate) fn set_snapshot_hook(&self, hook: impl FnMut(SnapshotPoint) + Send + 'static) { + *self.hooks.0.lock().expect("snapshot hook lock poisoned") = Some(Box::new(hook)); + } + + /// Whether the cursor-space lock is held right now, by anyone. + #[cfg(test)] + pub(crate) fn cursor_space_locked(&self) -> bool { + matches!( + self.seq.inner.try_lock(), + Err(std::sync::TryLockError::WouldBlock) + ) + } +} + +/// Receivers from [`TracingLogBus::subscribe_resumable`] and the mark that +/// separates buffered-only history from events queued on them. +pub(crate) struct ResumableSubscription { + pub(crate) log_rx: Option>, + pub(crate) platform_rx: Option>, + /// Highest seq the space had issued when the receivers subscribed; `0` + /// with no space. + pub(crate) subscribed_at: u64, +} + +/// Points inside the snapshot paths where a test hook runs. +#[cfg(test)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SnapshotPoint { + /// After `subscribe_resumable` subscribed and released the lock. + AfterSubscribe, + /// Inside `snapshot_tail` or `snapshot_after`, after the log read and + /// before the platform read, with the cursor-space lock still held. + BetweenReads, +} + +/// Test-only hook slot. Production builds carry no field at all. +#[cfg(test)] +#[derive(Clone, Default)] +#[allow(clippy::type_complexity)] +struct SnapshotHooks(Arc>>>); + +#[cfg(test)] +impl SnapshotHooks { + fn run(&self, point: SnapshotPoint) { + if let Some(hook) = self.0.lock().expect("snapshot hook lock poisoned").as_mut() { + hook(point); + } + } } -/// Outcome of [`TracingLogBus::snapshot_tail`]. Each floor is the one -/// `tail_with_floor_impl` reports for that source; an unread source has no -/// events and a floor of `0`. -pub(crate) struct TailSnapshot { - pub(crate) logs: Vec, - pub(crate) log_floor: u64, +#[cfg(test)] +impl std::fmt::Debug for SnapshotHooks { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("SnapshotHooks") + } +} + +/// One source's part of a [`TailSnapshot`]. An unread source has no events, +/// a floor of `0`, and nothing evicted. +#[derive(Debug, Default)] +pub(crate) struct SourceTail { pub(crate) events: Vec, - pub(crate) platform_floor: u64, + /// What `tail_with_floor_impl` reports for this source. + pub(crate) floor: u64, + /// The buffer trimmed at least one event published after the receivers + /// subscribed, before the snapshot could read it. Its live copy is + /// suppressed with the rest of the batch's range, so it is lost. + pub(crate) evicted_while_opening: bool, +} + +/// What [`race_publish_between_reads`] observed from inside the snapshot. +#[cfg(test)] +#[derive(Debug, Default)] +pub(crate) struct RaceProbe { + /// The hook ran at all, i.e. a snapshot reached its second read. + pub(crate) fired: bool, + /// The cursor-space lock was held between the two reads. + pub(crate) lock_held_between_reads: bool, + /// The competing publisher completed while the snapshot sat between its + /// reads. With both reads under one lock hold it cannot. + pub(crate) publisher_finished_between_reads: bool, + publisher: Option>, +} + +#[cfg(test)] +impl RaceProbe { + /// Wait for the competing publisher to finish both publishes. + pub(crate) fn join_publisher(&mut self) { + if let Some(handle) = self.publisher.take() { + handle.join().expect("publisher thread panicked"); + } + } +} + +/// Force the interleaving a snapshot must be atomic against: when the next +/// snapshot on `bus` sits between its log read and its platform read, start a +/// publisher that publishes one log line and then one platform event for +/// `sandbox_id`, and give it time to finish before letting the snapshot read +/// the platform bus. +/// +/// With separate reads, the publisher completes in that pause and the platform +/// read picks up its event while the log read missed the log line published +/// just before it. With one lock hold, the publisher blocks until the snapshot +/// is done, and both events land after it. The hook records what it saw +/// rather than asserting, because it may run inside a gateway producer task +/// where a panic would surface only as a truncated stream. +/// +/// The hook fires once. +#[cfg(test)] +pub(crate) fn race_publish_between_reads( + bus: &TracingLogBus, + sandbox_id: &str, +) -> Arc> { + use std::sync::mpsc; + use std::time::Duration; + + let probe = Arc::new(Mutex::new(RaceProbe::default())); + let hook_probe = Arc::clone(&probe); + let publisher_bus = bus.clone(); + let lock_bus = bus.clone(); + let sandbox_id = sandbox_id.to_string(); + // Closure state, not the probe, decides whether the hook already fired: + // a later snapshot must not touch the probe, which the test may be + // holding while it inspects the result. + let mut fired = false; + bus.set_snapshot_hook(move |point| { + if point != SnapshotPoint::BetweenReads || fired { + return; + } + fired = true; + let mut probe = hook_probe.lock().expect("race probe lock poisoned"); + probe.fired = true; + probe.lock_held_between_reads = lock_bus.cursor_space_locked(); + + let (started_tx, started_rx) = mpsc::channel(); + let (done_tx, done_rx) = mpsc::channel(); + let bus = publisher_bus.clone(); + let id = sandbox_id.clone(); + probe.publisher = Some(std::thread::spawn(move || { + started_tx.send(()).expect("hook waits for start"); + bus.publish_external(SandboxLogLine { + sandbox_id: id.clone(), + level: "INFO".to_string(), + target: "race".to_string(), + message: "raced log".to_string(), + source: "gateway".to_string(), + ..Default::default() + }); + bus.platform_event_bus.publish( + &id, + SandboxStreamEvent { + payload: Some(openshell_core::proto::sandbox_stream_event::Payload::Event( + openshell_core::proto::PlatformEvent { + source: "race".to_string(), + r#type: "Normal".to_string(), + reason: "raced".to_string(), + message: "raced event".to_string(), + ..Default::default() + }, + )), + cursor: String::new(), + }, + ); + let _ = done_tx.send(()); + })); + started_rx.recv().expect("publisher thread starts"); + // Long enough for an unblocked publisher to finish two publishes many + // times over. A blocked one never does, so the correct implementation + // always pays the full wait; a broken one fails reliably. + probe.publisher_finished_between_reads = + done_rx.recv_timeout(Duration::from_millis(300)).is_ok(); + }); + probe +} + +/// Outcome of [`TracingLogBus::snapshot_tail`]. +pub(crate) struct TailSnapshot { + pub(crate) log: SourceTail, + pub(crate) platform: SourceTail, /// Highest seq the space had issued at the lock hold; `0` with no space. pub(crate) highest_seq: u64, } @@ -714,7 +947,7 @@ mod tests { #[test] fn tail_with_floor_impl_empty_tail_returns_zero_floor() { let tail = VecDeque::new(); - let (events, floor) = tail_with_floor_impl(&tail, 10); + let (events, floor) = tail_with_floor_impl(&tail, 10, u64::MAX); assert!(events.is_empty()); assert_eq!(floor, 0); } @@ -722,12 +955,12 @@ mod tests { #[test] fn tail_with_floor_impl_max_covers_everything_returns_zero_floor() { let tail = tail_of(1, 5); - let (events, floor) = tail_with_floor_impl(&tail, 5); + let (events, floor) = tail_with_floor_impl(&tail, 5, u64::MAX); assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); assert_eq!(floor, 0); // Asking for more than exists is the same as asking for everything. - let (events, floor) = tail_with_floor_impl(&tail, 99); + let (events, floor) = tail_with_floor_impl(&tail, 99, u64::MAX); assert_eq!(cursors(&events), vec![1, 2, 3, 4, 5]); assert_eq!(floor, 0); } @@ -738,7 +971,7 @@ mod tests { // Newest 2 returned (4, 5); 1..=3 excluded, newest of those is 3. // Every returned event sits above the floor, so a bus never // withholds its own delivered window. - let (events, floor) = tail_with_floor_impl(&tail, 2); + let (events, floor) = tail_with_floor_impl(&tail, 2, u64::MAX); assert_eq!(cursors(&events), vec![4, 5]); assert_eq!(floor, 3); assert!(events.iter().all(|cursored| cursored.seq > floor)); @@ -749,46 +982,188 @@ mod tests { let tail = tail_of(1, 5); // Nothing returned; every event is excluded, so the floor is the // newest of them, 5. - let (events, floor) = tail_with_floor_impl(&tail, 0); + let (events, floor) = tail_with_floor_impl(&tail, 0, u64::MAX); assert!(events.is_empty()); assert_eq!(floor, 5); } - /// `snapshot_after` must take the same lock `publish` holds across its own - /// tail insert, so a publish can never land between the log read and the - /// platform read. Proving this directly (rather than racing threads and - /// hoping to catch a bad interleaving) is what makes this test reliable: - /// hold the lock ourselves and show `snapshot_after` cannot proceed until - /// it is released. + /// The resume read is atomic against a publish on either bus. The forced + /// interleaving publishes a log line and then a platform event while + /// `snapshot_after` sits between its two reads. If the reads were + /// separate, the platform read would return the event while the log read + /// had already missed the line before it: a client handed that event's + /// cursor would resume past the line forever. Under one lock hold the + /// publisher waits, the replay holds neither, and both arrive live in + /// cursor order and are replayable from the replay's last cursor. #[test] - fn snapshot_after_blocks_while_cursor_space_is_locked() { + fn snapshot_after_holds_the_lock_across_a_publish_between_reads() { let bus = TracingLogBus::new(); - let sandbox_id = "sb-lock"; - bus.publish_external(make_log_event(sandbox_id, "line")); + let sandbox_id = "sb-race-resume"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 let epoch = bus.cursor_space(sandbox_id).expect("space exists").epoch; + let sub = bus.subscribe_resumable(sandbox_id, true, true); + let (mut log_rx, mut platform_rx) = (sub.log_rx.unwrap(), sub.platform_rx.unwrap()); - let guard = bus.seq.lock(); + let probe = race_publish_between_reads(&bus, sandbox_id); + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 0, 0, true, true) + else { + panic!("expected a successful read"); + }; + { + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + } - let bus2 = bus.clone(); - let (done_tx, done_rx) = std::sync::mpsc::channel(); - let handle = std::thread::spawn(move || { - bus2.snapshot_after(sandbox_id, epoch, 0, 0, true, true); - done_tx.send(()).unwrap(); - }); + let log = log.expect("followed").expect("no gap"); + let platform = platform.expect("followed").expect("no gap"); + assert_eq!(cursors(&log), vec![1]); + assert_eq!(cursors(&platform), vec![2]); - // Give the worker time to reach (and block on) the lock. - std::thread::sleep(std::time::Duration::from_millis(50)); + // The raced events come live, in order, above the replay. + assert_eq!(log_rx.try_recv().expect("raced log is live").seq, 3); + assert_eq!(platform_rx.try_recv().expect("raced event is live").seq, 4); + + // Resuming from the replay's highest cursor recovers both. + let ResumeSnapshot::Read(log, platform) = + bus.snapshot_after(sandbox_id, epoch, 2, 2, true, true) + else { + panic!("expected a successful read"); + }; + assert_eq!(cursors(&log.unwrap().unwrap()), vec![3]); + assert_eq!(cursors(&platform.unwrap().unwrap()), vec![4]); + } + + /// The same forced interleaving on the initial path. Under one lock hold + /// the raced events are newer than the snapshot's high-water mark, so the + /// caller's cutoff lets their live copies through, in order. + #[test] + fn snapshot_tail_holds_the_lock_across_a_publish_between_reads() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-race-tail"; + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 2 + let sub = bus.subscribe_resumable(sandbox_id, true, true); + let (mut log_rx, mut platform_rx) = (sub.log_rx.unwrap(), sub.platform_rx.unwrap()); + + let probe = race_publish_between_reads(&bus, sandbox_id); + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(10), sub.subscribed_at); + let mut probe = probe.lock().unwrap(); + probe.join_publisher(); + assert!(probe.fired, "the hook must run between the two reads"); + assert!(probe.lock_held_between_reads, "lock released between reads"); + assert!( + !probe.publisher_finished_between_reads, + "a publish landed between the log read and the platform read" + ); + + assert_eq!(cursors(&snap.log.events), vec![1]); + assert_eq!(cursors(&snap.platform.events), vec![2]); + assert_eq!(snap.highest_seq, 2); + + let live_log = log_rx.try_recv().expect("raced log is live"); + let live_event = platform_rx.try_recv().expect("raced event is live"); + assert_eq!((live_log.seq, live_event.seq), (3, 4)); + assert!(live_log.seq > snap.highest_seq && live_event.seq > snap.highest_seq); + } + + /// `subscribe_resumable` records the mark under the lock every publish + /// holds across its send, so an event at or below it is never queued on + /// the new receivers and one above it always is. + #[test] + fn subscribe_resumable_splits_buffered_history_from_queued_events() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-sub"; assert_eq!( - done_rx.try_recv(), - Err(std::sync::mpsc::TryRecvError::Empty), - "snapshot_after returned without waiting for the cursor-space lock" + bus.subscribe_resumable(sandbox_id, true, true) + .subscribed_at, + 0 ); + assert_eq!(bus.cursor_space(sandbox_id), None, "must not mint a space"); - drop(guard); - done_rx - .recv_timeout(std::time::Duration::from_secs(1)) - .expect("snapshot_after should complete once the lock is released"); - handle.join().unwrap(); + bus.publish_external(make_log_event(sandbox_id, "l1")); // seq 1 + let sub = bus.subscribe_resumable(sandbox_id, true, false); + assert_eq!(sub.subscribed_at, 1); + assert!(sub.platform_rx.is_none()); + let mut log_rx = sub.log_rx.unwrap(); + assert!(log_rx.try_recv().is_err(), "history is not queued live"); + + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + assert_eq!(log_rx.try_recv().unwrap().seq, 2); + } + + /// The reviewer's example at bus level: subscribe, then platform event 1 + /// and log 2 are published before the snapshot. Both are queued live, and + /// the caller will suppress those copies, so the snapshot must return both + /// even though `event_tail` asked for no platform history -- and with + /// nothing excluded, neither source reports a floor. + #[test] + fn snapshot_tail_keeps_events_published_after_subscribe_regardless_of_depth() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-opening"; + let sub = bus.subscribe_resumable(sandbox_id, true, true); + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 1 + bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 + + let snap = bus.snapshot_tail(sandbox_id, Some(200), Some(0), sub.subscribed_at); + assert_eq!(cursors(&snap.log.events), vec![2]); + assert_eq!(cursors(&snap.platform.events), vec![1]); + assert_eq!((snap.log.floor, snap.platform.floor), (0, 0)); + assert!(!snap.log.evicted_while_opening && !snap.platform.evicted_while_opening); + assert_eq!(snap.highest_seq, 2); + } + + /// The exemption covers only events after the mark: older history is + /// still bounded by depth and still reports its floor. + #[test] + fn tail_with_floor_impl_exempts_only_events_after_keep_after() { + let tail = tail_of(1, 6); + // Depth 1 would return just 6; everything after 3 is exempt. + let (events, floor) = tail_with_floor_impl(&tail, 1, 3); + assert_eq!(cursors(&events), vec![4, 5, 6]); + assert_eq!(floor, 3); + + // A depth deeper than the exempt run still reaches into history. + let (events, floor) = tail_with_floor_impl(&tail, 4, 5); + assert_eq!(cursors(&events), vec![3, 4, 5, 6]); + assert_eq!(floor, 2); + + // Nothing after the mark: plain depth bound. + let (events, floor) = tail_with_floor_impl(&tail, 0, 6); + assert!(events.is_empty()); + assert_eq!(floor, 6); + } + + /// If the buffer trimmed an event published after subscribe before the + /// snapshot read it, that event is gone from both paths; the source says + /// so for the caller to report. + #[test] + fn snapshot_tail_reports_eviction_of_events_published_after_subscribe() { + let bus = TracingLogBus::new(); + let sandbox_id = "sb-opening-evict"; + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 1 + let sub = bus.subscribe_resumable(sandbox_id, true, true); + for _ in 0..=PlatformEventBus::DEFAULT_TAIL { + bus.platform_event_bus.publish(sandbox_id, stream_event(0)); + } + + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), sub.subscribed_at); + assert!(snap.platform.evicted_while_opening); + assert!(!snap.log.evicted_while_opening); + assert_eq!(snap.platform.events.len(), PlatformEventBus::DEFAULT_TAIL); + + // History trimmed before subscribe is ordinary truncation. + let late = bus.subscribe_resumable(sandbox_id, true, true); + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), late.subscribed_at); + assert!(!snap.platform.evicted_while_opening); + assert!(snap.platform.events.is_empty()); } /// Baseline correctness: interleaved log/platform publishes draw from the @@ -985,14 +1360,14 @@ mod tests { for _ in 0..5 { bus.publish_external(make_log_event(sandbox_id, "line")); } - let snap = bus.snapshot_tail(sandbox_id, Some(2), None); - assert_eq!(cursors(&snap.logs), vec![4, 5]); - assert_eq!(snap.log_floor, 3); + let snap = bus.snapshot_tail(sandbox_id, Some(2), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![4, 5]); + assert_eq!(snap.log.floor, 3); // Wide enough to cover everything: no floor. - let snap = bus.snapshot_tail(sandbox_id, Some(10), None); - assert_eq!(cursors(&snap.logs), vec![1, 2, 3, 4, 5]); - assert_eq!(snap.log_floor, 0); + let snap = bus.snapshot_tail(sandbox_id, Some(10), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1, 2, 3, 4, 5]); + assert_eq!(snap.log.floor, 0); } /// `highest_seq` is the space's high-water mark at the lock hold, not the @@ -1007,11 +1382,11 @@ mod tests { bus.publish_external(make_log_event(sandbox_id, "l2")); // seq 2 bus.platform_event_bus.publish(sandbox_id, stream_event(0)); // seq 3 - let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0)); - assert_eq!(cursors(&snap.logs), vec![1, 2]); - assert_eq!(snap.log_floor, 0); - assert!(snap.events.is_empty()); - assert_eq!(snap.platform_floor, 3); + let snap = bus.snapshot_tail(sandbox_id, Some(10), Some(0), u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1, 2]); + assert_eq!(snap.log.floor, 0); + assert!(snap.platform.events.is_empty()); + assert_eq!(snap.platform.floor, 3); assert_eq!(snap.highest_seq, 3); } @@ -1024,52 +1399,18 @@ mod tests { bus.publish_external(make_log_event(sandbox_id, "l1")); bus.platform_event_bus.publish(sandbox_id, stream_event(0)); - let snap = bus.snapshot_tail(sandbox_id, Some(10), None); - assert_eq!(cursors(&snap.logs), vec![1]); - assert!(snap.events.is_empty()); - assert_eq!(snap.platform_floor, 0); + let snap = bus.snapshot_tail(sandbox_id, Some(10), None, u64::MAX); + assert_eq!(cursors(&snap.log.events), vec![1]); + assert!(snap.platform.events.is_empty()); + assert_eq!(snap.platform.floor, 0); assert_eq!(snap.highest_seq, 2); - let snap = bus.snapshot_tail("nope", Some(10), Some(10)); - assert!(snap.logs.is_empty() && snap.events.is_empty()); + let snap = bus.snapshot_tail("nope", Some(10), Some(10), u64::MAX); + assert!(snap.log.events.is_empty() && snap.platform.events.is_empty()); assert_eq!(snap.highest_seq, 0); assert_eq!(bus.cursor_space("nope"), None); } - /// Same guarantee as `snapshot_after_blocks_while_cursor_space_is_locked` - /// for the initial read: `snapshot_tail` takes the lock every publish holds - /// across its tail insert and broadcast send, so no publish can land - /// between the log read and the platform read. - #[test] - fn snapshot_tail_blocks_while_cursor_space_is_locked() { - let bus = TracingLogBus::new(); - let sandbox_id = "sb-tail-lock"; - bus.publish_external(make_log_event(sandbox_id, "line")); - - let guard = bus.seq.lock(); - - let bus2 = bus.clone(); - let (done_tx, done_rx) = std::sync::mpsc::channel(); - let handle = std::thread::spawn(move || { - bus2.snapshot_tail(sandbox_id, Some(10), Some(10)); - done_tx.send(()).unwrap(); - }); - - // Give the worker time to reach (and block on) the lock. - std::thread::sleep(std::time::Duration::from_millis(50)); - assert_eq!( - done_rx.try_recv(), - Err(std::sync::mpsc::TryRecvError::Empty), - "snapshot_tail returned without waiting for the cursor-space lock" - ); - - drop(guard); - done_rx - .recv_timeout(std::time::Duration::from_secs(1)) - .expect("snapshot_tail should complete once the lock is released"); - handle.join().unwrap(); - } - #[test] fn platform_event_bus_tail_with_floor_reports_truncation() { let bus = TracingLogBus::new(); @@ -1479,7 +1820,7 @@ impl PlatformEventBus { let inner = self.inner.lock().expect("platform event bus lock poisoned"); inner.per_id.get(sandbox_id).map_or_else( || (Vec::new(), 0), - |per| tail_with_floor_impl(&per.tail, max), + |per| tail_with_floor_impl(&per.tail, max, u64::MAX), ) } diff --git a/docs/observability/accessing-logs.mdx b/docs/observability/accessing-logs.mdx index 2f4c7a15ae..869b166320 100644 --- a/docs/observability/accessing-logs.mdx +++ b/docs/observability/accessing-logs.mdx @@ -60,6 +60,8 @@ The gateway merges the log and platform event sources before emitting, so events A cursor is one position shared by the log and platform sources; you track a single highest value, not one per source. If you request both logs and platform events, either source can deliver fewer events on connect than its replay depth asked for. This happens when the other source's replay leaves out an event that is newer than part of this source's history, even when both depths are equal. The gateway withholds those events rather than hand out a cursor that one of the two sources cannot fully vouch for, and reports what it withheld with a warning event rather than dropping it silently. Watches that follow only logs, such as `openshell logs`, are not affected. +Replay depths bound history only. An event published while the stream is opening is always part of the initial batch, so the batch can hold more events than requested. If the gateway's buffer evicts such an event before the batch is read, the stream reports it with a warning instead. + ## Direct Filesystem Access Start an independent shell with `sandbox exec` to read log files directly: diff --git a/proto/openshell.proto b/proto/openshell.proto index 621306002d..bebbdb66a4 100644 --- a/proto/openshell.proto +++ b/proto/openshell.proto @@ -2024,10 +2024,13 @@ message WatchSandboxRequest { // When following both sources, the replay can return fewer lines: events // older than one the other source's replay left out are withheld and // reported with a warning, even if both depths are equal. + // + // The depth bounds history only. Events published while the stream is + // opening are always included, so the replay can return more than N. uint32 log_tail_lines = 5; // Replay the last N platform events (best-effort) before following. - // Defaults to 0. Can return fewer events; see log_tail_lines. + // Defaults to 0. Can return fewer or more events; see log_tail_lines. uint32 event_tail = 6; // Stop streaming once the sandbox reaches READY or a terminal result phase From 6d39167b483f625b66c62d4e9af1e9b99e023d0c Mon Sep 17 00:00:00 2001 From: Artem Lytvyn Date: Sun, 8 Nov 2026 23:38:22 +0000 Subject: [PATCH 31/31] chore(sdk): drop generated Go binding changes Signed-off-by: Artem Lytvyn --- sdk/go/proto/openshellv1/openshell.pb.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/sdk/go/proto/openshellv1/openshell.pb.go b/sdk/go/proto/openshellv1/openshell.pb.go index 6c9b4e144c..109d2ef454 100644 --- a/sdk/go/proto/openshellv1/openshell.pb.go +++ b/sdk/go/proto/openshellv1/openshell.pb.go @@ -7659,13 +7659,8 @@ type WatchSandboxRequest struct { // Stream platform events correlated to this sandbox. FollowEvents bool `protobuf:"varint,4,opt,name=follow_events,json=followEvents,proto3" json:"follow_events,omitempty"` // Replay the last N log lines (best-effort) before following. - // - // When following both sources, the replay can return fewer lines: events - // older than one the other source's replay left out are withheld and - // reported with a warning, even if both depths are equal. LogTailLines uint32 `protobuf:"varint,5,opt,name=log_tail_lines,json=logTailLines,proto3" json:"log_tail_lines,omitempty"` // Replay the last N platform events (best-effort) before following. - // Defaults to 0. Can return fewer events; see log_tail_lines. EventTail uint32 `protobuf:"varint,6,opt,name=event_tail,json=eventTail,proto3" json:"event_tail,omitempty"` // Stop streaming once the sandbox reaches READY or a terminal result phase // (COMPLETED, STOPPED, or ERROR). @@ -7694,8 +7689,6 @@ type WatchSandboxRequest struct { // empty resume_after_cursor, because retrying the same token fails // identically. A cursor this server could not have issued is rejected with // INVALID_ARGUMENT. - // - // One cursor covers both log and platform events. ResumeAfterCursor string `protobuf:"bytes,12,opt,name=resume_after_cursor,json=resumeAfterCursor,proto3" json:"resume_after_cursor,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache