|
| 1 | +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +//! Fixtures for tests that assert on gateway audit events. |
| 5 | +
|
| 6 | +use super::{ConnectionSource, RequestOrigin}; |
| 7 | +use openshell_ocsf::test_support::OcsfCapture; |
| 8 | +use openshell_ocsf::validation::schema::{ |
| 9 | + load_class_schema, validate_known_fields, validate_required_fields, |
| 10 | +}; |
| 11 | +use serde_json::Value; |
| 12 | +use std::future::Future; |
| 13 | + |
| 14 | +/// The `x-request-id` a test request carries. |
| 15 | +pub const REQUEST_ID: &str = "6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f"; |
| 16 | + |
| 17 | +/// The address a test request comes from. |
| 18 | +pub const PEER: &str = "203.0.113.7:51234"; |
| 19 | + |
| 20 | +/// The IP address of [`PEER`] as events record it. |
| 21 | +pub const PEER_IP: &str = "203.0.113.7"; |
| 22 | + |
| 23 | +/// A direct connection from [`PEER`]. |
| 24 | +pub fn peer() -> ConnectionSource { |
| 25 | + ConnectionSource::Peer(PEER.parse().expect("peer address")) |
| 26 | +} |
| 27 | + |
| 28 | +/// A request from [`PEER`] carrying [`REQUEST_ID`]. |
| 29 | +pub fn origin() -> RequestOrigin { |
| 30 | + let mut headers = http::HeaderMap::new(); |
| 31 | + headers.insert("x-request-id", REQUEST_ID.parse().expect("request ID")); |
| 32 | + RequestOrigin::new(Some(peer()), &headers) |
| 33 | +} |
| 34 | + |
| 35 | +/// Run `handler` as a routed request to `path` from [`origin`], returning |
| 36 | +/// its output and the OCSF events it emitted. |
| 37 | +pub async fn audited<F: Future>(path: &str, handler: F) -> (F::Output, Vec<Value>) { |
| 38 | + let capture = OcsfCapture::default(); |
| 39 | + let output = { |
| 40 | + let _guard = capture.install(); |
| 41 | + super::with_request(path.to_string(), origin(), handler).await |
| 42 | + }; |
| 43 | + (output, capture.json_events()) |
| 44 | +} |
| 45 | + |
| 46 | +/// Assert that `event` records the request from [`origin`]. |
| 47 | +pub fn assert_origin(event: &Value) { |
| 48 | + assert_eq!(event["src_endpoint"]["ip"], PEER_IP, "{event}"); |
| 49 | + assert_eq!(event["metadata"]["correlation_uid"], REQUEST_ID, "{event}"); |
| 50 | +} |
| 51 | + |
| 52 | +/// Assert that `event` never mentions `secret`. |
| 53 | +pub fn assert_redacted(event: &Value, secret: &str) { |
| 54 | + assert!( |
| 55 | + !event.to_string().contains(secret), |
| 56 | + "event mentions {secret}: {event}" |
| 57 | + ); |
| 58 | +} |
| 59 | + |
| 60 | +fn assert_conforms(event: &Value, class: &str) { |
| 61 | + let schema = load_class_schema(class); |
| 62 | + validate_required_fields(event, &schema); |
| 63 | + validate_known_fields(event, &schema); |
| 64 | +} |
| 65 | + |
| 66 | +/// Assert that `event` is an Authentication failure for `reason` on a |
| 67 | +/// request to `path`. |
| 68 | +pub fn assert_authentication_failure(event: &Value, path: &str, reason: &str) { |
| 69 | + assert_conforms(event, "authentication"); |
| 70 | + assert_eq!(event["class_uid"], 3002, "{event}"); |
| 71 | + assert_eq!(event["activity_id"], 1, "{event}"); |
| 72 | + assert_eq!(event["status_id"], 2, "{event}"); |
| 73 | + assert_eq!(event["action_id"], 2, "{event}"); |
| 74 | + assert_eq!(event["disposition_id"], 2, "{event}"); |
| 75 | + assert_eq!(event["status_detail"], reason, "{event}"); |
| 76 | + assert_eq!(event["service"]["name"], "openshell-gateway", "{event}"); |
| 77 | + assert_eq!(event["http_request"]["url"]["path"], path, "{event}"); |
| 78 | +} |
| 79 | + |
| 80 | +/// Assert that `event` is an Authentication logon for a request to `path`. |
| 81 | +pub fn assert_logon(event: &Value, path: &str) { |
| 82 | + assert_conforms(event, "authentication"); |
| 83 | + assert_eq!(event["class_uid"], 3002, "{event}"); |
| 84 | + assert_eq!(event["activity_id"], 1, "{event}"); |
| 85 | + assert_eq!(event["status_id"], 1, "{event}"); |
| 86 | + assert_eq!(event["action_id"], 1, "{event}"); |
| 87 | + assert_eq!(event["disposition_id"], 1, "{event}"); |
| 88 | + assert_eq!(event["severity_id"], 1, "{event}"); |
| 89 | + assert_eq!(event["service"]["name"], "openshell-gateway", "{event}"); |
| 90 | + assert_eq!(event["http_request"]["url"]["path"], path, "{event}"); |
| 91 | +} |
| 92 | + |
| 93 | +/// Assert that `event` is an API Activity denial for `reason` on a request |
| 94 | +/// to `path`. |
| 95 | +pub fn assert_denial(event: &Value, path: &str, reason: &str) { |
| 96 | + assert_conforms(event, "api_activity"); |
| 97 | + assert_eq!(event["class_uid"], 6003, "{event}"); |
| 98 | + assert_eq!(event["action_id"], 2, "{event}"); |
| 99 | + assert_eq!(event["disposition_id"], 2, "{event}"); |
| 100 | + assert_eq!(event["status_id"], 2, "{event}"); |
| 101 | + assert_eq!(event["status_detail"], reason, "{event}"); |
| 102 | + assert_eq!(event["http_request"]["url"]["path"], path, "{event}"); |
| 103 | +} |
| 104 | + |
| 105 | +/// Assert that `finding` is the sandbox boundary finding paired with |
| 106 | +/// `denial`. |
| 107 | +pub fn assert_boundary_finding(denial: &Value, finding: &Value) { |
| 108 | + assert_conforms(finding, "detection_finding"); |
| 109 | + assert_eq!(finding["class_uid"], 2004, "{finding}"); |
| 110 | + assert_eq!( |
| 111 | + finding["finding_info"]["uid"], "sandbox-authorization-boundary", |
| 112 | + "{finding}" |
| 113 | + ); |
| 114 | + assert_eq!(finding["severity_id"], 4, "{finding}"); |
| 115 | + assert_eq!(finding["is_alert"], true, "{finding}"); |
| 116 | + let correlation_uid = &denial["metadata"]["correlation_uid"]; |
| 117 | + assert!(correlation_uid.is_string(), "{denial}"); |
| 118 | + assert_eq!( |
| 119 | + &finding["metadata"]["correlation_uid"], correlation_uid, |
| 120 | + "{finding}" |
| 121 | + ); |
| 122 | +} |
0 commit comments