diff --git a/CHANGELOG.md b/CHANGELOG.md index ec347fd9..5566dcc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). the load with the options listed, so an incomplete audit configuration no longer looks like a working one. `stderr` is still available and has to be named. ([#237](https://github.com/praxis-proxy/policy/issues/237)) +- **Breaking for Rust callers of `praxis-policy-plugin-ocsf-audit`:** + `OcsfAuditConfig.destination` and `.exporters` become one + `output: OutputSelection`, and the struct no longer implements `Default`; + build one with `OcsfAuditConfig::new`. YAML keys and serialized shape are + unchanged; a block that names no output, both, or an empty list now fails at + parse. ([#264](https://github.com/praxis-proxy/policy/issues/264)) - **Breaking: CIBA config rejects unknown fields.** A misspelled key such as `stroe` now fails at load and names the field. ([#236](https://github.com/praxis-proxy/policy/issues/236)) diff --git a/Cargo.lock b/Cargo.lock index 95ba4635..2b28e84e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3042,6 +3042,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "thiserror", "tokio", "tracing", "uuid", diff --git a/reference/plugins/ocsf-audit/Cargo.toml b/reference/plugins/ocsf-audit/Cargo.toml index 445359b5..69782de5 100644 --- a/reference/plugins/ocsf-audit/Cargo.toml +++ b/reference/plugins/ocsf-audit/Cargo.toml @@ -43,6 +43,7 @@ ryu = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } +thiserror = { workspace = true } # The audit destination runs on blocking workers so a stalled write cannot # pin the executor's async audit timeout. tokio = { workspace = true } diff --git a/reference/plugins/ocsf-audit/src/config.rs b/reference/plugins/ocsf-audit/src/config.rs index 6186cefd..3446e35b 100644 --- a/reference/plugins/ocsf-audit/src/config.rs +++ b/reference/plugins/ocsf-audit/src/config.rs @@ -9,6 +9,7 @@ use std::sync::Arc; use praxis_policy_core::audit::export::{AuditExporter, ExportError, ExporterConfig}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; +use thiserror::Error; use crate::emitter::TracingExporter; @@ -22,35 +23,36 @@ const MISSING_OUTPUT: &str = "no audit destination configured; set `destination: /// The `config:` block of an `audit/ocsf` plugin entry. /// -/// `destination` or `exporters` must be set before the sink can load. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] +/// Holds exactly one output. In YAML the entry sets exactly one of +/// `destination` and `exporters`, and an `exporters` list is never empty. +/// Build one in code with [`OcsfAuditConfig::new`]. +/// +/// ``` +/// use praxis_policy_plugin_ocsf_audit::{OcsfAuditConfig, OcsfDestination, OutputSelection}; +/// +/// let mut config = OcsfAuditConfig::new(OutputSelection::Destination(OcsfDestination::Stderr)); +/// config.chain_uid = Some("gateway-eu-1".to_owned()); +/// assert!(config.chain, "chaining is on unless the entry turns it off"); +/// ``` +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(into = "WireConfig", try_from = "WireConfig")] pub struct OcsfAuditConfig { - /// Where OCSF events go, as a single destination: `stderr` or - /// `tracing`. Mutually exclusive with `exporters`, which can name - /// several. One of the two is required. - #[serde(default)] - pub destination: Option, - - /// Where OCSF events go, as a list of exporters. Every record is - /// written, byte for byte, to each one. Mutually exclusive with - /// `destination`; an empty list is rejected. + /// Where OCSF events go: a single `destination`, `stderr` or `tracing`, + /// or an `exporters` list that can name several. The entry sets one of + /// the two keys. /// /// ```yaml /// exporters: /// - { kind: file, path: /var/log/ppe/ocsf.ndjson, fsync: batch } /// - { kind: stderr } /// ``` - #[serde(default)] - pub exporters: Option>, + pub output: OutputSelection, /// Populates OCSF `metadata.product` so a single collector can /// attribute events to a deployment. - #[serde(default = "default_product_name")] pub product_name: String, /// Populates OCSF `metadata.product.vendor_name`. - #[serde(default = "default_vendor_name")] pub vendor_name: String, /// When true, attach an attestation to every event: compute a @@ -58,7 +60,6 @@ pub struct OcsfAuditConfig { /// event through `prev_event` (its uid, `type_uid` and fingerprint), /// forming a tamper-evident chain. This declares the /// `record_integrity` profile. - #[serde(default = "default_true")] pub chain: bool, /// Identifier for this attestation chain (OCSF @@ -73,7 +74,6 @@ pub struct OcsfAuditConfig { /// `metadata.uid` discards the later records as /// replays. Give every process its own value, or leave it unset and /// take the per-process default. - #[serde(default)] pub chain_uid: Option, /// Signing mode for the attestation. `none` produces an unsigned @@ -85,7 +85,6 @@ pub struct OcsfAuditConfig { /// missing key fails construction loudly rather than silently /// emitting unsigned records. Through the factory, use the path form: /// the engine retains its plugin config. - #[serde(default)] pub signing: SigningMode, /// Inline PKCS#8 P-256 private key PEM for `signing: dsse`. @@ -98,12 +97,10 @@ pub struct OcsfAuditConfig { /// Consumed at direct construction: the emitter zeroizes and clears /// this field and its own raw plugin config, whatever `signing` is set /// to. A caller that kept another config copy must clear that copy. - #[serde(default)] pub signing_key_pem: Option, /// Path to a PKCS#8 P-256 private key PEM for `signing: dsse`. /// Mutually exclusive with `signing_key_pem`. - #[serde(default)] pub signing_key_pem_path: Option, /// Key identifier (JWKS `kid`) stamped at `unmapped.signature_key_id`, @@ -111,7 +108,6 @@ pub struct OcsfAuditConfig { /// published key set. Rides in `unmapped`, outside the hashed bytes /// like the signature itself, because `digital_signature` has no /// member for signature material. - #[serde(default)] pub signing_key_id: Option, /// OCSF `attestation.authority_uid`: the authority the signing @@ -122,7 +118,6 @@ pub struct OcsfAuditConfig { /// serialization, so it cannot be swapped after the fact without /// breaking the fingerprint. Recommended in the schema; set it /// whenever signing is on. - #[serde(default)] pub authority_uid: Option, /// When true (default), fields that have no native OCSF home @@ -130,15 +125,16 @@ pub struct OcsfAuditConfig { /// security labels) are emitted under OCSF `unmapped` rather than /// dropped. That preserves the evidence and keeps the schema gaps /// visible in the records themselves. - #[serde(default = "default_true")] pub include_gap_fields: bool, } -impl Default for OcsfAuditConfig { - fn default() -> Self { +impl OcsfAuditConfig { + /// A config that writes to `output`, with every other field at its + /// default. + #[must_use] + pub fn new(output: OutputSelection) -> Self { Self { - destination: None, - exporters: None, + output, product_name: default_product_name(), vendor_name: default_vendor_name(), chain: default_true(), @@ -153,14 +149,85 @@ impl Default for OcsfAuditConfig { } } -fn default_product_name() -> String { - "Praxis Policy Engine OCSF Audit".to_owned() +/// Where the sink writes: one destination, or a list of exporters that each +/// receive every record byte for byte. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OutputSelection { + /// The `destination` key: `stderr` or `tracing`. + Destination(OcsfDestination), + /// The `exporters` key: one or more exporters. + Exporters(ExporterList), } -fn default_vendor_name() -> String { - "Praxis".to_owned() + +impl OutputSelection { + /// The exporters this selection names, opened and ready to write. + /// + /// # Errors + /// + /// Returns an error if an exporter cannot be opened, such as a `file` + /// path that is not absolute or not writable. + pub fn build_exporters(&self) -> Result>, ExportError> { + match self { + Self::Destination(OcsfDestination::Tracing) => Ok(vec![Arc::new(TracingExporter)]), + Self::Destination(OcsfDestination::Stderr) => Ok(vec![ExporterConfig::Stderr.build()?]), + Self::Exporters(list) => list + .as_slice() + .iter() + .map(OcsfExporterConfig::build) + .collect(), + } + } } -fn default_true() -> bool { - true + +/// A nonempty `exporters` list. +/// +/// ``` +/// use praxis_policy_plugin_ocsf_audit::{ +/// ExporterList, OcsfExporterConfig, OutputSelectionError, TracingTag, +/// }; +/// +/// let list = ExporterList::try_from(vec![OcsfExporterConfig::Tracing(TracingTag::Tracing)])?; +/// assert_eq!(list.as_slice().len(), 1); +/// assert!( +/// matches!(ExporterList::try_from(vec![]), Err(OutputSelectionError::Empty)), +/// "an empty list is refused" +/// ); +/// # Ok::<(), OutputSelectionError>(()) +/// ``` +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ExporterList(Vec); + +impl ExporterList { + /// The entries, in the order records are written to them. + #[must_use] + pub fn as_slice(&self) -> &[OcsfExporterConfig] { + &self.0 + } +} + +impl TryFrom> for ExporterList { + type Error = OutputSelectionError; + + fn try_from(entries: Vec) -> Result { + if entries.is_empty() { + return Err(OutputSelectionError::Empty); + } + Ok(Self(entries)) + } +} + +/// Why a `config:` block does not select an output. +#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)] +pub enum OutputSelectionError { + /// Both `destination` and `exporters` are set. + #[error("set either `destination` or `exporters`, not both")] + Conflicting, + /// `exporters` is set but names nothing. + #[error("`exporters` is empty; name at least one of stderr, stdout, file or tracing")] + Empty, + /// Neither key is set; null counts as unset. + #[error("{MISSING_OUTPUT}")] + Missing, } /// Where a record is written when `destination` is set. @@ -228,19 +295,6 @@ impl OcsfExporterConfig { } } -impl OcsfAuditConfig { - /// The exporters records go to: `exporters` when set, otherwise the one - /// `destination` names. - /// - /// # Errors - /// - /// Returns an error if neither output is set, both are set, the exporter - /// list is empty, or an exporter cannot be opened. - pub fn build_exporters(&self) -> Result>, ExportError> { - OutputSelection::try_from(self)?.build_exporters() - } -} - /// How the attestation is signed. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -255,46 +309,300 @@ pub enum SigningMode { Dsse, } -enum OutputSelection<'a> { - Destination(OcsfDestination), - Exporters { - first: &'a OcsfExporterConfig, - rest: &'a [OcsfExporterConfig], - }, +/// The `config:` block as the entry writes it, with `destination` and +/// `exporters` as two keys. The checked conversion into [`OcsfAuditConfig`] +/// is where an entry that selects no single output is refused. +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct WireConfig { + #[serde(default)] + authority_uid: Option, + #[serde(default = "default_true")] + chain: bool, + #[serde(default)] + chain_uid: Option, + #[serde(default)] + destination: Option, + #[serde(default)] + exporters: Option>, + #[serde(default = "default_true")] + include_gap_fields: bool, + #[serde(default = "default_product_name")] + product_name: String, + #[serde(default)] + signing: SigningMode, + #[serde(default)] + signing_key_id: Option, + #[serde(default)] + signing_key_pem: Option, + #[serde(default)] + signing_key_pem_path: Option, + #[serde(default = "default_vendor_name")] + vendor_name: String, } -impl<'a> TryFrom<&'a OcsfAuditConfig> for OutputSelection<'a> { - type Error = ExportError; - - fn try_from(config: &'a OcsfAuditConfig) -> Result { - match (&config.destination, &config.exporters) { - (None, None) => Err(ExportError::Config(MISSING_OUTPUT.to_owned())), - (Some(_), Some(_)) => Err(ExportError::Config( - "set either `destination` or `exporters`, not both".to_owned(), - )), - (None, Some(list)) => { - let Some((first, rest)) = list.split_first() else { - return Err(ExportError::Config( - "`exporters` is empty; name at least one of stderr, stdout, file or tracing" - .to_owned(), - )); - }; - Ok(Self::Exporters { first, rest }) - }, - (Some(destination), None) => Ok(Self::Destination(*destination)), +impl TryFrom for OcsfAuditConfig { + type Error = OutputSelectionError; + + fn try_from(wire: WireConfig) -> Result { + let output = match (wire.destination, wire.exporters) { + (None, None) => return Err(OutputSelectionError::Missing), + (Some(_), Some(_)) => return Err(OutputSelectionError::Conflicting), + (Some(destination), None) => OutputSelection::Destination(destination), + (None, Some(entries)) => OutputSelection::Exporters(ExporterList::try_from(entries)?), + }; + Ok(Self { + output, + product_name: wire.product_name, + vendor_name: wire.vendor_name, + chain: wire.chain, + chain_uid: wire.chain_uid, + signing: wire.signing, + signing_key_pem: wire.signing_key_pem, + signing_key_pem_path: wire.signing_key_pem_path, + signing_key_id: wire.signing_key_id, + authority_uid: wire.authority_uid, + include_gap_fields: wire.include_gap_fields, + }) + } +} + +impl From for WireConfig { + fn from(config: OcsfAuditConfig) -> Self { + let (destination, exporters) = match config.output { + OutputSelection::Destination(destination) => (Some(destination), None), + OutputSelection::Exporters(list) => (None, Some(list.0)), + }; + Self { + authority_uid: config.authority_uid, + chain: config.chain, + chain_uid: config.chain_uid, + destination, + exporters, + include_gap_fields: config.include_gap_fields, + product_name: config.product_name, + signing: config.signing, + signing_key_id: config.signing_key_id, + signing_key_pem: config.signing_key_pem, + signing_key_pem_path: config.signing_key_pem_path, + vendor_name: config.vendor_name, } } } -impl OutputSelection<'_> { - fn build_exporters(self) -> Result>, ExportError> { - match self { - Self::Destination(OcsfDestination::Tracing) => Ok(vec![Arc::new(TracingExporter)]), - Self::Destination(OcsfDestination::Stderr) => Ok(vec![ExporterConfig::Stderr.build()?]), - Self::Exporters { first, rest } => std::iter::once(first) - .chain(rest.iter()) - .map(OcsfExporterConfig::build) - .collect(), +// ----------------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------------- + +fn default_product_name() -> String { + "Praxis Policy Engine OCSF Audit".to_owned() +} + +fn default_vendor_name() -> String { + "Praxis".to_owned() +} + +fn default_true() -> bool { + true +} + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn a_config_built_in_code_matches_a_destination_only_block() -> TestResult { + let built = OcsfAuditConfig::new(OutputSelection::Destination(OcsfDestination::Stderr)); + let parsed: OcsfAuditConfig = serde_json::from_value(json!({ "destination": "stderr" }))?; + + assert_eq!( + serde_json::to_value(built)?, + serde_json::to_value(parsed)?, + "code and YAML must agree on every default" + ); + Ok(()) + } + + #[test] + fn each_destination_builds_its_one_exporter() -> TestResult { + let stderr = OutputSelection::Destination(OcsfDestination::Stderr).build_exporters()?; + let tracing = OutputSelection::Destination(OcsfDestination::Tracing).build_exporters()?; + + assert_eq!( + exporter_names(&stderr), + ["stderr"], + "destination stderr must build one stderr exporter" + ); + assert_eq!( + exporter_names(&tracing), + ["tracing"], + "destination tracing must build one tracing exporter" + ); + Ok(()) + } + + #[test] + fn an_exporter_list_builds_every_entry_in_order() -> TestResult { + let list = ExporterList::try_from(vec![ + OcsfExporterConfig::Tracing(TracingTag::Tracing), + OcsfExporterConfig::Builtin(ExporterConfig::Stdout), + ])?; + let exporters = OutputSelection::Exporters(list).build_exporters()?; + + assert_eq!( + exporter_names(&exporters), + ["tracing", "stdout"], + "exporters must build in list order" + ); + Ok(()) + } + + #[test] + fn an_empty_exporter_list_cannot_be_constructed() { + assert!( + matches!( + ExporterList::try_from(vec![]), + Err(OutputSelectionError::Empty) + ), + "an empty list must be refused at construction" + ); + } + + #[test] + fn missing_conflicting_null_and_empty_selections_fail_to_parse() { + for (block, expected) in [ + (json!({}), OutputSelectionError::Missing), + (json!({ "chain": true }), OutputSelectionError::Missing), + ( + json!({ "destination": null }), + OutputSelectionError::Missing, + ), + (json!({ "exporters": null }), OutputSelectionError::Missing), + ( + json!({ "destination": null, "exporters": null }), + OutputSelectionError::Missing, + ), + ( + json!({ "destination": "stderr", "exporters": [{ "kind": "stderr" }] }), + OutputSelectionError::Conflicting, + ), + (json!({ "exporters": [] }), OutputSelectionError::Empty), + ] { + let message = serde_json::from_value::(block.clone()) + .err() + .map(|err| err.to_string()); + + assert_eq!( + message, + Some(expected.to_string()), + "{block} must fail with the {expected:?} message" + ); + } + } + + #[test] + fn the_missing_output_error_lists_every_option() { + let message = OutputSelectionError::Missing.to_string(); + + for needle in [ + "destination", + "exporters", + "stderr", + "tracing", + "stdout", + "file", + ] { + assert!( + message.contains(needle), + "the error must name {needle}: {message}" + ); } } + + #[test] + fn unknown_keys_are_still_rejected() { + let message = serde_json::from_value::(json!({ + "destination": "stderr", + "destinations": "stdout", + })) + .err() + .map(|err| err.to_string()); + + assert!( + message + .as_deref() + .is_some_and(|text| text.contains("destinations")), + "an unknown key must fail the parse and be named: {message:?}" + ); + } + + #[test] + fn a_null_beside_a_set_key_is_ignored() -> TestResult { + let parsed: OcsfAuditConfig = serde_json::from_value(json!({ + "destination": null, + "exporters": [{ "kind": "stdout" }], + }))?; + + assert_eq!( + parsed.output, + OutputSelection::Exporters(ExporterList::try_from(vec![OcsfExporterConfig::Builtin( + ExporterConfig::Stdout + )])?), + "an explicit null must read as absent" + ); + Ok(()) + } + + #[test] + fn both_shapes_round_trip_on_the_wire() -> TestResult { + let destination = json!({ "destination": "tracing", "chain": false }); + let exporters = json!({ + "exporters": [{ "kind": "tracing" }, { "kind": "stdout" }], + "vendor_name": "Acme", + }); + + for block in [destination, exporters] { + let parsed: OcsfAuditConfig = serde_json::from_value(block.clone())?; + let wire = serde_json::to_value(&parsed)?; + let reparsed: OcsfAuditConfig = serde_json::from_value(wire.clone())?; + + for (key, value) in block.as_object().into_iter().flatten() { + assert_eq!( + wire.get(key), + Some(value), + "{key} must serialize as it was written" + ); + } + assert_eq!( + wire.as_object().map(serde_json::Map::len), + Some(12), + "the wire shape keeps both output keys: {wire}" + ); + assert_eq!( + serde_json::to_value(reparsed)?, + wire, + "a serialized config must parse back to the same wire form" + ); + } + Ok(()) + } + + // ----------------------------------------------------------------------------- + // Test Utilities + // ----------------------------------------------------------------------------- + + type TestResult = Result<(), Box>; + + fn exporter_names(exporters: &[Arc]) -> Vec { + exporters + .iter() + .map(|exporter| exporter.name().to_owned()) + .collect() + } } diff --git a/reference/plugins/ocsf-audit/src/emitter.rs b/reference/plugins/ocsf-audit/src/emitter.rs index 9de6a10d..9a099091 100644 --- a/reference/plugins/ocsf-audit/src/emitter.rs +++ b/reference/plugins/ocsf-audit/src/emitter.rs @@ -137,21 +137,19 @@ impl OcsfAuditEmitter { /// Returns an error for invalid configuration, a missing output, an /// unavailable exporter, or invalid signing keys. pub fn new(mut cfg: PluginConfig) -> Result> { - let mut typed: OcsfAuditConfig = match cfg.config.as_ref() { - Some(raw) => serde_json::from_value(raw.clone()).map_err(|e| { - Box::new(PluginError::Config { - message: format!( - "plugin '{}' (praxis-policy-plugin-ocsf-audit) config parse failed: {e}", - cfg.name - ), - }) - })?, - None => OcsfAuditConfig::default(), - }; + let raw = cfg.config.clone().unwrap_or_else(|| json!({})); + let mut typed: OcsfAuditConfig = serde_json::from_value(raw).map_err(|e| { + Box::new(PluginError::Config { + message: format!( + "plugin '{}' (praxis-policy-plugin-ocsf-audit) config parse failed: {e}", + cfg.name + ), + }) + })?; // Opened here so an unwritable file fails the load, not the first // verdict. - let exporters = typed.build_exporters().map_err(|e| { + let exporters = typed.output.build_exporters().map_err(|e| { Box::new(PluginError::Config { message: format!( "plugin '{}' (praxis-policy-plugin-ocsf-audit): {e}", @@ -1044,20 +1042,6 @@ mod tests { ); } - /// `OcsfAuditConfig::default()` is what an entry with no `config:` block - /// gets, and deserializing `{}` is what an empty block gets. A field - /// whose `serde(default)` and struct default disagree splits the two - /// paths silently, which is how `chain` came to be off on one of them. - #[test] - fn struct_defaults_match_an_empty_config_block() { - let from_empty: OcsfAuditConfig = serde_json::from_value(json!({})).unwrap(); - assert_eq!( - serde_json::to_value(OcsfAuditConfig::default()).unwrap(), - serde_json::to_value(from_empty).unwrap(), - "the struct default and an empty config block must agree" - ); - } - /// A key configured with `signing: none` is never consumed by a signer, /// which is what an operator leaves behind when signing is turned off /// for a rotation. It must be cleared all the same: both config copies @@ -1961,6 +1945,7 @@ mod tests { fn exporter_names(typed: &OcsfAuditConfig) -> Vec { typed + .output .build_exporters() .unwrap() .iter() @@ -1974,21 +1959,6 @@ mod tests { .to_string() } - #[test] - fn destination_and_exporters_together_are_rejected() { - let err = exporter_err(json!({ - "destination": "stderr", - "exporters": [{ "kind": "stderr" }], - })); - assert!(err.contains("not both"), "{err}"); - } - - #[test] - fn an_empty_exporter_list_is_rejected() { - let err = exporter_err(json!({ "exporters": [] })); - assert!(err.contains("empty"), "{err}"); - } - #[test] fn an_unwritable_file_fails_construction() { let err = exporter_err(json!({ @@ -2025,24 +1995,13 @@ mod tests { #[test] fn with_no_destination_or_exporters_the_sink_refuses_to_load() { - let err = OcsfAuditConfig::default().build_exporters().unwrap_err(); - let msg = err.to_string(); - for needle in [ - "destination", - "exporters", - "stderr", - "tracing", - "stdout", - "file", - ] { - assert!(msg.contains(needle), "the error must name {needle}: {msg}"); - } - let mut absent = cfg(json!({})); absent.config = None; let mut silent = cfg(json!({})); silent.config = Some(json!({ "chain": true })); - for c in [absent, silent] { + let mut blank = cfg(json!({})); + blank.config = Some(json!({ "destination": null, "exporters": null })); + for c in [absent, silent, blank] { let err = OcsfAuditEmitter::new(c).unwrap_err().to_string(); assert!( err.contains("no audit destination configured"), @@ -2051,24 +2010,6 @@ mod tests { } } - #[test] - fn an_explicit_destination_is_the_one_exporter() { - let stderr: OcsfAuditConfig = - serde_json::from_value(json!({ "destination": "stderr" })).unwrap(); - assert_eq!( - exporter_names(&stderr), - ["stderr"], - "destination: stderr must build one stderr exporter" - ); - let tracing: OcsfAuditConfig = - serde_json::from_value(json!({ "destination": "tracing" })).unwrap(); - assert_eq!( - exporter_names(&tracing), - ["tracing"], - "destination: tracing must build one tracing exporter" - ); - } - #[test] fn an_unknown_destination_is_rejected() { let err = exporter_err(json!({ "destination": "carrier-pigeon" })); diff --git a/reference/plugins/ocsf-audit/src/lib.rs b/reference/plugins/ocsf-audit/src/lib.rs index 5b2652e0..e1618d80 100644 --- a/reference/plugins/ocsf-audit/src/lib.rs +++ b/reference/plugins/ocsf-audit/src/lib.rs @@ -42,6 +42,9 @@ pub mod factory; pub mod ocsf; pub mod sign; -pub use config::{OcsfAuditConfig, OcsfDestination, SigningMode}; +pub use config::{ + ExporterList, OcsfAuditConfig, OcsfDestination, OcsfExporterConfig, OutputSelection, + OutputSelectionError, SigningMode, TracingTag, +}; pub use emitter::OcsfAuditEmitter; pub use factory::{KIND, OcsfAuditFactory};