cedar_local_agent/public/log/
schema.rs

1//! Defines the `OpenCyberSecurityFramework` schema and associated types and helpers to convert
2//! cedar authorization inputs to a log format.
3use std::collections::HashMap;
4use std::fmt;
5use std::fmt::{Display, Formatter};
6use std::time::Instant;
7
8use cedar_policy::{Diagnostics, Entities, EntityUid, Request, Response};
9use chrono::{Local, Utc};
10use derive_builder::Builder;
11use serde::{Deserialize, Serialize};
12use serde_json::{to_value, Map, Value};
13use serde_repr::{Deserialize_repr, Serialize_repr};
14
15use crate::public::log::error::OcsfException;
16use crate::public::log::error::OcsfException::OcsfFieldsValidationError;
17use crate::public::log::{FieldLevel, FieldSet};
18
19/// The maximum allowed enrichment array size
20const ALLOWED_ENRICHMENT_ARRAY_LEN: usize = 5;
21/// The maximum activity name length
22const ALLOWED_ACTIVITY_NAME_LEN: usize = 35;
23
24/// The OCSF schema version
25const OCSF_SCHEMA_VERSION: &str = "1.0.0";
26/// The Log version
27const LOG_VERSION: &str = "1.0.0";
28/// The vendor name
29const VENDOR_NAME: &str = "cedar::simple::authorizer";
30/// String used for redaction
31const SECRET_STRING: &str = "Sensitive<REDACTED>";
32
33/// A basic Open Cyber Security Framework structure
34///
35/// Entity Management events report activity. The activity can be a
36/// create, read, update, and delete operation on a managed entity.
37///
38/// <https://schema.ocsf.io/1.0.0/classes/entity_management?extensions=>
39#[derive(Default, Builder, Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
40#[builder(
41    setter(into),
42    build_fn(validate = "Self::validate_ocsf_fields", error = "OcsfException")
43)]
44pub struct OpenCyberSecurityFramework {
45    /// The event activity name, as defined by the `activity_id`
46    #[builder(default)]
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub activity_name: Option<String>,
49    /// The activity id enum
50    pub activity_id: ActivityId,
51    /// The event category name, as defined by `category_uid` value: Identity & Access Management
52    #[builder(default = "Some(\"Identity & Access Management\".to_string())")]
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub category_name: Option<String>,
55    /// The category unique identifier of the event. The authorization log will always be 3
56    #[builder(default = "3u8")]
57    pub category_uid: u8,
58    /// The event class name, as defined by `class_uid` value: `Entity Management`
59    #[builder(default = "Some(\"Entity Management\".to_string())")]
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub class_name: Option<String>,
62    /// The unique identifier of a class. By default, the value is set to 3004
63    #[builder(default = "3004u64")]
64    pub class_uid: u64,
65    /// The user provided comment about why the entity was changed
66    #[builder(default)]
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub comment: Option<String>,
69    /// The number of times that events in the same logical group occurred during the event Start
70    /// time to End Time period.
71    #[builder(default)]
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub count: Option<u64>,
74    /// The event duration or aggregate time, the amount of time the event covers from `start_time` to `end_time` in milliseconds
75    #[builder(default)]
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub duration: Option<i64>,
78    /// The end time of a time period, or the time of the most recent event included in the aggregate event
79    #[builder(default)]
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub end_time: Option<i64>,
82    /// The additional information from an external data source, which is associated with the event
83    /// for example add location information for the IP address.
84    #[builder(default)]
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub enrichments: Option<Vec<EnrichmentArray>>,
87    /// The principal entity that is sending the request
88    pub entity: ManagedEntity,
89    /// The resource entity that is being acted upon
90    #[builder(default)]
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub entity_result: Option<ManagedEntity>,
93    /// The timestamp value indicating the time of the event occurrence in UTC
94    pub time: i64,
95    /// The event description will either display an error message if one exists, or
96    /// provide detailed information on how the request was authorized in the absence of an error
97    #[builder(default)]
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub message: Option<String>,
100    /// The metadata associated with the event such as the `log_provider`
101    pub metadata: MetaData,
102    /// The observables associated with the event
103    #[builder(default)]
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub observables: Option<Vec<Observable>>,
106    /// The event data as received from the event source
107    #[builder(default)]
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub raw_data: Option<String>,
110    /// The event severity, normalized to the caption of the `severity_id` value
111    #[builder(default)]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub severity: Option<String>,
114    /// The normalized identifier of the event severity
115    pub severity_id: SeverityId,
116    /// The start time of an event
117    #[builder(default)]
118    #[serde(skip_serializing_if = "Option::is_none")]
119    pub start_time: Option<i64>,
120    /// The event status, normalized to the caption of the `status_id` value
121    #[builder(default)]
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub status: Option<String>,
124    /// The event status code, as reported by the event source
125    #[builder(default)]
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub status_code: Option<String>,
128    /// The status details contains additional information about the event outcome
129    #[builder(default)]
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub status_detail: Option<String>,
132    /// The normalized identifier of the event status
133    #[builder(default)]
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub status_id: Option<StatusId>,
136    /// The number of minutes that the reported event time is ahead or behind UTC,
137    /// in the range -1,080 to +1,080
138    #[builder(default)]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub timezone_offset: Option<i32>,
141    /// The event type ID. It identifies the event's semantics and structure.
142    /// the value is calculated by the logging system as: `class_uid` * 100 + `activity_id`
143    pub type_uid: TypeUid,
144    /// The event type name, as defined by the `type_uid`
145    #[builder(default)]
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub type_name: Option<String>,
148    /// The attributes that are not mapped to the event schema.
149    /// the names and values of those attributes are specific to the event source
150    #[builder(default)]
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub unmapped: Option<Value>,
153}
154
155impl OpenCyberSecurityFramework {
156    /// Converts Request, Entities, Field Set into a filtered OCSF log.
157    ///
158    /// # Errors
159    ///
160    /// Will return `OcsfException` if `ProductBuilder`, `MetaDataBuilder`, `ManagedEntityBuilder`
161    /// failed to build the models, Serde failed to deserializing the object, Cedar residual
162    /// evaluation request and any model validation error
163    pub fn create(
164        request: &Request,
165        response: &Response,
166        entities: &Entities,
167        fields: &FieldSet,
168        authorizer_name: &str,
169    ) -> Result<Self, OcsfException> {
170        let decision = response.decision();
171        Self::create_generic(
172            request,
173            response.diagnostics(),
174            format!("decision is {decision:?}").as_str(),
175            format!("{decision:?}"),
176            entities,
177            fields,
178            authorizer_name,
179        )
180    }
181
182    /// Converts Request, Entities, Field Set into a filtered OCSF log.
183    ///
184    /// # Errors
185    ///
186    /// Will return `OcsfException` if `ProductBuilder`, `MetaDataBuilder`, `ManagedEntityBuilder`
187    /// failed to build the models, Serde failed to deserializing the object and any model validation error
188    pub fn create_generic(
189        request: &Request,
190        diagnostics: &Diagnostics,
191        outcome: &str,
192        status_code: String,
193        entities: &Entities,
194        fields: &FieldSet,
195        authorizer_name: &str,
196    ) -> Result<Self, OcsfException> {
197        let filtered_request = filter_request(request, entities, fields);
198        let start_time = Instant::now();
199
200        let mut unmapped = Map::new();
201        if let Some(context_str) = filtered_request.clone().context {
202            unmapped.insert("context".to_string(), to_value(context_str)?);
203        } else {
204            unmapped.insert("context".to_string(), to_value(SECRET_STRING)?);
205        }
206
207        // Cedar will return None when the evaluation is only partial. This check will raise an error
208        // and not continue to obtain the entity information below. Consider remove this check after
209        // this issue resolved. https://github.com/cedar-policy/cedar/issues/72
210        let principal = filtered_request.principal.get_id();
211        let action = filtered_request.action.get_id();
212        let resource = filtered_request.resource.get_id();
213
214        let reasons: Vec<String> = diagnostics.reason().map(ToString::to_string).collect();
215        unmapped.insert(
216            "determined_policies".to_string(),
217            to_value(reasons.clone())?,
218        );
219
220        let response_error: Vec<String> = diagnostics
221            .errors()
222            .map(std::string::ToString::to_string)
223            .collect();
224        unmapped.insert(
225            "evaluation_errors".to_string(),
226            to_value(response_error.clone())?,
227        );
228
229        let (status_id, status, status_details);
230
231        if response_error.is_empty() {
232            status_id = StatusId::Success;
233            status = "Success".to_string();
234            status_details = reasons.join(",");
235        } else {
236            status_id = StatusId::Failure;
237            status = "Failure".to_string();
238            status_details = response_error.join(",");
239        }
240
241        let message = format!(
242            "Principal {principal} performed action \
243                {action} on {resource}, the {outcome} \
244                determined by policy id {reasons:?} and errors {response_error:?}",
245        );
246
247        let product = ProductBuilder::default()
248            .vendor_name(VENDOR_NAME)
249            .name(authorizer_name.to_string())
250            .lang("en".to_string())
251            .build()?;
252
253        let (severity_id, severity) = build_ocsf_severity(response_error.len());
254
255        let source_entity = generate_managed_entity(
256            filtered_request.entities.as_ref(),
257            &filtered_request.principal,
258        )?;
259        let resource_entity = generate_managed_entity(
260            filtered_request.entities.as_ref(),
261            &filtered_request.resource,
262        )?;
263        let action_entity =
264            generate_managed_entity(filtered_request.entities.as_ref(), &filtered_request.action)?;
265        unmapped.insert(
266            "action_entity_details".to_string(),
267            to_value(action_entity)?,
268        );
269
270        let timezone_offset = Local::now().offset().local_minus_utc() / 60;
271
272        let activity_id = ActivityId::from(action.to_string());
273        let type_uid = TypeUid::from(activity_id.clone());
274
275        let metadata = MetaDataBuilder::default()
276            .version(OCSF_SCHEMA_VERSION)
277            .product(product)
278            .log_provider(VENDOR_NAME.to_string())
279            .logged_time(Utc::now().timestamp())
280            .log_version(LOG_VERSION.to_string())
281            .processed_time(start_time.elapsed().as_millis())
282            .build()?;
283
284        OpenCyberSecurityFrameworkBuilder::default()
285            .activity_name(action)
286            .activity_id(activity_id)
287            .entity(source_entity)
288            .entity_result(resource_entity)
289            .message(message)
290            .type_uid(type_uid.clone())
291            .type_name(type_uid.to_string())
292            .severity(severity)
293            .severity_id(severity_id)
294            .metadata(metadata)
295            .time(Utc::now().timestamp())
296            .timezone_offset(timezone_offset)
297            .status_id(status_id)
298            .status(status)
299            .status_detail(status_details)
300            .status_code(status_code)
301            .unmapped(to_value(unmapped)?)
302            .build()
303    }
304
305    /// A default error implementation provided.
306    pub fn error(error_message: String, authorizer_name: String) -> Self {
307        let product = ProductBuilder::default()
308            .vendor_name(VENDOR_NAME)
309            .name(authorizer_name)
310            .build()
311            .unwrap_or_default();
312
313        OpenCyberSecurityFrameworkBuilder::default()
314            .type_uid(TypeUid::Other)
315            .severity_id(SeverityId::Other)
316            .metadata(
317                MetaDataBuilder::default()
318                    .version(OCSF_SCHEMA_VERSION)
319                    .product(product)
320                    .build()
321                    .unwrap_or_default(),
322            )
323            .time(Utc::now().timestamp())
324            .entity(
325                ManagedEntityBuilder::default()
326                    .name("N/A".to_string())
327                    .build()
328                    .unwrap_or_default(),
329            )
330            .activity_id(ActivityId::Other)
331            .message(error_message)
332            .build()
333            .unwrap_or_default()
334    }
335}
336
337fn filter_request(request: &Request, entities: &Entities, fields: &FieldSet) -> FilteredRequest {
338    let mut builder = FilteredRequestBuilder::default();
339
340    if fields.principal {
341        builder.principal(request.principal().cloned());
342    }
343    if fields.action {
344        builder.action(request.action().cloned());
345    }
346    if fields.resource {
347        builder.resource(request.resource().cloned());
348    }
349
350    // Since there is no `Context` getter on the `Request`, instead return `request.to_string()`
351    // which includes the context.
352    if fields.context {
353        builder.context(request.to_string());
354    }
355
356    let entities = match fields.entities {
357        FieldLevel::All => Some(entities.clone()),
358        FieldLevel::Custom(filter_fn) => Some(filter_fn(entities)),
359        FieldLevel::None | FieldLevel::Unknown => None,
360    };
361
362    builder.entities(entities);
363    builder.build().unwrap_or_default()
364}
365
366fn generate_managed_entity(
367    entities: Option<&Entities>,
368    component: &EntityComponent,
369) -> Result<ManagedEntity, OcsfException> {
370    // The map contains the useful information of entity. For now, it only contains the ancestors
371    // information and could add more in the future such as entity attributes.
372    let mut entity_details_map = Map::new();
373
374    let mut parents = Vec::<String>::new();
375    if let EntityComponent::Concrete(entity_uid) = component {
376        parents = entities.as_ref().map_or_else(Vec::<String>::new, |e| {
377            e.ancestors(entity_uid)
378                .map_or_else(Vec::<String>::new, |e| e.map(ToString::to_string).collect())
379        });
380    }
381
382    entity_details_map.insert("Parents".to_string(), to_value(parents)?);
383    Ok(ManagedEntityBuilder::default()
384        .name(component.get_id())
385        .entity_type(component.get_type_name())
386        .data(to_value(entity_details_map)?)
387        .build()?)
388}
389
390fn build_ocsf_severity(num_of_errors: usize) -> (SeverityId, String) {
391    match num_of_errors {
392        0 => (SeverityId::Informational, "Informational".to_string()),
393        1 => (SeverityId::Low, "Low".to_string()),
394        _ => (SeverityId::Medium, "Medium".to_string()),
395    }
396}
397
398impl OpenCyberSecurityFrameworkBuilder {
399    /// Validates inputs to the builder for potential denial of service length inputs.
400    ///
401    /// # Errors
402    ///
403    /// If the `OpenCyberSecurityFrameworkBuilder` does not pass validation checks, like exceeding
404    /// the maximum allowed length for the `activity_name`, it will result in an `OcsfFieldsValidationError`.
405    fn validate_ocsf_fields(&self) -> Result<(), OcsfException> {
406        let is_enrichments_valid: bool = self.enrichments.as_ref().is_none_or(|enrichments| {
407            enrichments
408                .as_ref()
409                .is_none_or(|vec| vec.len() < ALLOWED_ENRICHMENT_ARRAY_LEN)
410        });
411
412        let is_activity_name_valid: bool =
413            self.activity_name.as_ref().is_none_or(|activity_name| {
414                activity_name
415                    .as_ref()
416                    .is_none_or(|s| s.len() < ALLOWED_ACTIVITY_NAME_LEN)
417            });
418
419        if is_enrichments_valid && is_activity_name_valid {
420            Ok(())
421        } else {
422            Err(OcsfFieldsValidationError(format!(
423                "Either the Enrichments array exceeds the maximum allowed size of \
424	                 {ALLOWED_ENRICHMENT_ARRAY_LEN} elements, or the Activity name exceeds the \
425	                 maximum allowed length of {ALLOWED_ACTIVITY_NAME_LEN} characters... "
426            )))
427        }
428    }
429}
430
431/// The normalized identifier of the activity that triggered the event
432#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
433#[repr(u8)]
434pub enum ActivityId {
435    /// The event is unknown
436    #[default]
437    Unknown = 0,
438    /// The activity is creating some resources
439    Create = 1,
440    /// The activity is read-only operation
441    Read = 2,
442    /// The activity is updating existing resource
443    Update = 3,
444    /// The activity is deleting resource
445    Delete = 4,
446    /// The event activity is not mapped
447    Other = 99,
448}
449
450/// The 1 to 1 mapping of `activity_name` to the `ActivityId`
451impl From<String> for ActivityId {
452    fn from(activity_name: String) -> Self {
453        let activity_name_lower_case = activity_name.to_lowercase();
454
455        match activity_name_lower_case.as_str() {
456            "read" => Self::Read,
457            "update" => Self::Update,
458            "delete" => Self::Delete,
459            "unknown" => Self::Unknown,
460            _ => Self::Other,
461        }
462    }
463}
464
465/// The normalized severity is a measurement the effort and expense required to manage and resolve
466/// an event or incident
467#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
468#[repr(u8)]
469pub enum SeverityId {
470    /// The event severity is not known
471    #[default]
472    Unknown = 0,
473    /// Informational message. No action required
474    Informational = 1,
475    /// The user decides if action is needed
476    Low = 2,
477    /// Action is required but the situation is not serious at this time
478    Medium = 3,
479    /// Action is required immediately
480    High = 4,
481    /// Action is required immediately and the scope is broad
482    Critical = 5,
483    /// An error occurred but it is too late to take remedial action
484    Fatal = 6,
485    /// The event severity is not mapped. See the severity attribute, which contains a data source specific value
486    Other = 99,
487}
488
489/// The normalized identifier of the event status
490#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
491#[repr(u8)]
492pub enum StatusId {
493    /// The status is unknown
494    #[default]
495    Unknown = 0,
496    /// The event was successful
497    Success = 1,
498    /// The event failed
499    Failure = 2,
500    /// The event status is not mapped
501    Other = 99,
502}
503
504/// The event type ID. It identifies the event's semantics and structure
505#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
506#[repr(u64)]
507pub enum TypeUid {
508    /// The status is unknown
509    #[default]
510    Unknown = 300_400,
511    /// Create activity
512    Create = 300_401,
513    /// Read activity
514    Read = 300_402,
515    /// Update activity
516    Update = 300_403,
517    /// Delete activity
518    Delete = 300_404,
519    /// Other activity
520    Other = 300_499,
521}
522
523/// Enables an easy way to call `to_string` on `TypeUid`.
524impl fmt::Display for TypeUid {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        write!(f, "{self:?}")
527    }
528}
529
530/// Map the `ActivityId` to `TypeUid`. They have the same category but different values
531impl From<ActivityId> for TypeUid {
532    fn from(activity_id: ActivityId) -> Self {
533        match activity_id {
534            ActivityId::Unknown => Self::Unknown,
535            ActivityId::Create => Self::Create,
536            ActivityId::Read => Self::Read,
537            ActivityId::Update => Self::Update,
538            ActivityId::Delete => Self::Delete,
539            ActivityId::Other => Self::Other,
540        }
541    }
542}
543
544/// The observable value type identifier
545#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
546#[repr(u8)]
547pub enum ObservableTypeId {
548    /// Unknown observable data type
549    #[default]
550    Unknown = 0,
551    /// Unique name assigned to a device connected to a computer network
552    Hostname = 1,
553    /// Internet Protocol address (IP address), in either IPv4 or IPv6 format
554    IPAddress = 2,
555    /// Media Access Control (MAC) address. For example: 18:36:F3:98:4F:9A
556    MACAddress = 3,
557    #[allow(clippy::doc_markdown)]
558    /// User name. For example: john_doe
559    UserName = 4,
560    #[allow(clippy::doc_markdown)]
561    /// Email address. For example: john_doe@example.com
562    EmailAddress = 5,
563    /// Uniform Resource Locator (URL) string
564    URLString = 6,
565    /// File name. For example: text-file.txt
566    FileName = 7,
567    /// File hash. A unique value that corresponds to the content of the file
568    FileHash = 8,
569    /// Process name. For example: Notepad
570    ProcessName = 9,
571    /// Resource unique identifier. For example, S3 Bucket name or EC2 Instance ID
572    ResourceUID = 10,
573    /// Endpoints, whether physical or virtual, connect to and interact with computer networks.
574    /// Examples include mobile devices, computers, virtual machines, embedded devices, servers,
575    /// and `IoT` devices like cameras and smart speakers
576    Endpoint = 20,
577    /// The User object describes the characteristics of a user/person or a security principal.
578    /// Defined by D3FEND [d3f:UserAccount](https://d3fend.mitre.org/dao/artifact/d3f:UserAccount/)
579    User = 21,
580    /// The Email object describes the email metadata such as sender, recipients, and direction.
581    /// Defined by D3FEND [d3f:Email](https://d3fend.mitre.org/dao/artifact/d3f:Email/)
582    Email = 22,
583    /// The Uniform Resource Locator(URL) object describes the characteristics of a URL.
584    /// Defined in RFC 1738 and by D3FEND [d3f:URL](https://d3fend.mitre.org/dao/artifact/d3f:URL/)
585    UniformResourceLocator = 23,
586    /// The File object represents the metadata associated with a file stored in a computer system.
587    ///Defined by D3FEND [d3f:File](https://next.d3fend.mitre.org/dao/artifact/d3f:File/)
588    File = 24,
589    /// The Process object describes a running instance of a launched program.
590    /// Defined by D3FEND [d3f:Process](https://d3fend.mitre.org/dao/artifact/d3f:Process/)
591    Process = 25,
592    /// The Geo Location object describes a geographical location, usually associated with an IP address.
593    /// Defined by D3FEND [d3f:PhysicalLocation](https://d3fend.mitre.org/dao/artifact/d3f:PhysicalLocation/)
594    GeoLocation = 26,
595    /// The Container object describes an instance of a specific container
596    Container = 27,
597    /// The registry key object describes a Windows registry key.
598    /// Defined by D3FEND [d3f:WindowsRegistryKey](https://d3fend.mitre.org/dao/artifact/d3f:WindowsRegistryKey/)
599    RegistryKey = 28,
600    /// The registry value object describes a Windows registry value
601    RegistryValue = 29,
602    /// The Fingerprint object provides detailed information about a digital fingerprint,
603    /// which is a compact representation of data used to identify a longer piece of information,
604    /// such as a public key or file content
605    Fingerprint = 30,
606    /// The observable data type is not mapped
607    Other = 99,
608}
609
610/// The Reputation object describes the reputation/risk score of an entity (e.g. device, user, domain)
611#[derive(Debug, Serialize_repr, Deserialize_repr, Clone, Eq, PartialEq, Default)]
612#[repr(u8)]
613pub enum ReputationScoreId {
614    /// Unknown
615    #[default]
616    Unknown = 0,
617    /// Long history of good behavior
618    VerySafe = 1,
619    /// Consistently good behavior
620    Safe = 2,
621    /// No bad behavior
622    ProbablySafe = 3,
623    /// Reasonable history of good behavior
624    LeansSafe = 4,
625    /// Starting to establish a history behavior
626    MayNotBeSafe = 5,
627    /// No established history of normal behavior
628    ExerciseCaution = 6,
629    /// Starting to establish a history of suspicious or risky behavior
630    SuspiciousRisky = 7,
631    /// A site with a history of suspicious or risky behavior.
632    /// (spam, scam, potentially unwanted software, potentially malicious)
633    PossiblyMalicious = 8,
634    /// Strong possibility of maliciousness
635    ProbablyMalicious = 9,
636    /// Indicators of maliciousness
637    Malicious = 10,
638    /// Proven evidence of maliciousness
639    Other = 99,
640}
641
642/// The additional information from an external data source, which is associated with the event.
643/// <https://schema.ocsf.io/1.0.0/objects/enrichment?extensions=>
644#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
645#[builder(setter(into))]
646pub struct EnrichmentArray {
647    /// The enrichment data associated with the attribute and value
648    pub data: HashMap<String, Vec<String>>,
649    /// The name of the attribute to which the enriched data pertains
650    pub name: String,
651    /// The value of the attribute to which the enriched data pertains
652    pub value: String,
653    /// The enrichment data provider name
654    #[builder(default)]
655    #[serde(skip_serializing_if = "Option::is_none")]
656    pub provider: Option<String>,
657    /// The enrichment type. For example: location
658    #[serde(rename = "type")]
659    #[builder(default)]
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub enrichment_type: Option<String>,
662}
663
664/// A pivot element that contains related information found in many places in the event.
665/// <https://schema.ocsf.io/1.0.0/objects/observable?extensions=>
666#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
667#[builder(setter(into))]
668pub struct Observable {
669    /// The observable value type identifier
670    pub type_id: ObservableTypeId,
671    /// The full name of the observable attribute
672    pub name: String,
673    /// The value associated with the observable attribute. The meaning of the value depends on
674    /// the observable type
675    #[builder(default)]
676    #[serde(skip_serializing_if = "Option::is_none")]
677    pub value: Option<String>,
678    /// The observable value type name
679    #[serde(rename = "type")]
680    #[builder(default)]
681    #[serde(skip_serializing_if = "Option::is_none")]
682    pub observable_type: Option<String>,
683    /// Contains the original and normalized reputation scores
684    #[builder(default)]
685    #[serde(skip_serializing_if = "Option::is_none")]
686    pub reputation: Option<Reputation>,
687}
688
689/// Describes the reputation/risk score of an entity (e.g. device, user, domain).
690/// <https://schema.ocsf.io/1.0.0/objects/reputation?extensions=>
691#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
692#[builder(setter(into))]
693pub struct Reputation {
694    /// The normalized reputation score identifier
695    pub score_id: ReputationScoreId,
696    /// The reputation score as reported by the event source
697    pub base_score: u8,
698    /// The provider of the reputation information
699    #[builder(default)]
700    #[serde(skip_serializing_if = "Option::is_none")]
701    pub provider: Option<String>,
702    /// The reputation score, normalized to the caption of the `score_id` value. In the case of 'Other',
703    /// it is defined by the event source
704    #[builder(default)]
705    #[serde(skip_serializing_if = "Option::is_none")]
706    pub score: Option<String>,
707}
708
709/// The managed entity that is being acted upon.
710/// <https://schema.ocsf.io/1.0.0/objects/managed_entity?extensions=>
711#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
712#[builder(setter(into))]
713pub struct ManagedEntity {
714    /// The managed entity content as a JSON object
715    #[builder(default)]
716    #[serde(skip_serializing_if = "Option::is_none")]
717    pub data: Option<Value>,
718    /// The name of the managed entity
719    #[builder(default)]
720    #[serde(skip_serializing_if = "Option::is_none")]
721    pub name: Option<String>,
722    /// The managed entity namespace
723    #[serde(rename = "type")]
724    #[builder(default)]
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub entity_type: Option<String>,
727    /// The identifier of the managed entity
728    #[builder(default)]
729    #[serde(skip_serializing_if = "Option::is_none")]
730    pub unique_id: Option<String>,
731    /// The version of the managed entity
732    #[builder(default)]
733    #[serde(skip_serializing_if = "Option::is_none")]
734    pub version: Option<String>,
735}
736
737/// Describes the metadata associated with the event
738/// <https://schema.ocsf.io/1.0.0/objects/metadata?extensions=>
739#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
740#[builder(setter(into))]
741pub struct MetaData {
742    /// The version of the OCSF schema
743    pub version: String,
744    /// The product that reported the event
745    pub product: Product,
746    /// The original event time as reported by the event source. Omit if event is generated instead
747    /// of collected via logs
748    #[builder(default)]
749    #[serde(skip_serializing_if = "Option::is_none")]
750    pub original_time: Option<String>,
751    /// The logging provider or logging service that logged the event
752    #[builder(default)]
753    #[serde(skip_serializing_if = "Option::is_none")]
754    pub log_provider: Option<String>,
755    /// The event log name
756    #[builder(default)]
757    #[serde(skip_serializing_if = "Option::is_none")]
758    pub log_name: Option<String>,
759    /// Sequence number of the event. The sequence number is a value available in some events,
760    /// to make the exact ordering of events unambiguous, regardless of the event time precision
761    #[builder(default)]
762    #[serde(skip_serializing_if = "Option::is_none")]
763    pub sequence: Option<u64>,
764    /// The schema extension used to create the event
765    #[builder(default)]
766    #[serde(skip_serializing_if = "Option::is_none")]
767    pub extension: Option<Extension>,
768    /// The list of profiles used to create the event
769    #[builder(default)]
770    #[serde(skip_serializing_if = "Option::is_none")]
771    pub profiles: Option<Vec<String>>,
772    /// The event processed time, such as an ETL operation
773    #[builder(default)]
774    #[serde(skip_serializing_if = "Option::is_none")]
775    pub processed_time: Option<u128>,
776    /// The time when the event was last modified or enriched
777    #[builder(default)]
778    #[serde(skip_serializing_if = "Option::is_none")]
779    pub modified_time: Option<i64>,
780    /// The time when the logging system collected and logged the event
781    #[builder(default)]
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub logged_time: Option<i64>,
784    /// The event log schema version that specifies the format of the original event
785    #[builder(default)]
786    #[serde(skip_serializing_if = "Option::is_none")]
787    pub log_version: Option<String>,
788    // The list of category labels attached to the event or specific attributes
789    /// Labels are user defined tags or aliases added at normalization time
790    #[builder(default)]
791    #[serde(skip_serializing_if = "Option::is_none")]
792    pub labels: Option<Vec<String>>,
793    /// The logging system-assigned unique identifier of an event instance
794    #[builder(default)]
795    #[serde(skip_serializing_if = "Option::is_none")]
796    pub uid: Option<String>,
797    /// The Event ID or Code that the product uses to describe the event
798    #[builder(default)]
799    #[serde(skip_serializing_if = "Option::is_none")]
800    pub event_code: Option<String>,
801    /// The unique identifier used to correlate events
802    #[builder(default)]
803    #[serde(skip_serializing_if = "Option::is_none")]
804    pub correlation_uid: Option<String>,
805}
806
807/// Describes characteristics of a software product.
808/// <https://schema.ocsf.io/1.0.0/objects/product?extensions=>
809#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
810#[builder(setter(into))]
811pub struct Product {
812    /// The name of the vendor of the product
813    pub vendor_name: String,
814    /// The version of the product, as defined by the event source
815    #[builder(default)]
816    #[serde(skip_serializing_if = "Option::is_none")]
817    pub version: Option<String>,
818    /// The unique identifier of the product
819    #[builder(default)]
820    #[serde(skip_serializing_if = "Option::is_none")]
821    pub uid: Option<String>,
822    /// The name of the product
823    #[builder(default)]
824    #[serde(skip_serializing_if = "Option::is_none")]
825    pub name: Option<String>,
826    /// The two letter lower case language codes. For example, en(English)
827    #[builder(default)]
828    #[serde(skip_serializing_if = "Option::is_none")]
829    pub lang: Option<String>,
830    /// The URL pointing towards the product
831    #[builder(default)]
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub url_string: Option<String>,
834    /// The installation path of the product
835    #[builder(default)]
836    #[serde(skip_serializing_if = "Option::is_none")]
837    pub path: Option<String>,
838    /// The feature that reported the event
839    #[builder(default)]
840    #[serde(skip_serializing_if = "Option::is_none")]
841    pub feature: Option<Feature>,
842}
843
844/// Encompasses details related to the capabilities, components, user interface (UI) design,
845/// and performance upgrades associated with the feature.
846///
847/// <https://schema.ocsf.io/1.0.0/objects/feature?extensions=>
848#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
849#[builder(setter(into))]
850pub struct Feature {
851    /// The name of the feature
852    #[builder(default)]
853    #[serde(skip_serializing_if = "Option::is_none")]
854    pub name: Option<String>,
855    /// The unique identifier of the feature
856    #[builder(default)]
857    #[serde(skip_serializing_if = "Option::is_none")]
858    pub uid: Option<String>,
859    /// The version of the feature
860    #[builder(default)]
861    #[serde(skip_serializing_if = "Option::is_none")]
862    pub version: Option<String>,
863}
864
865/// Detailed information about the schema extension used to construct the event
866/// <https://schema.ocsf.io/1.0.0/objects/extension?extensions=>
867#[derive(Default, Serialize, Deserialize, Builder, Eq, PartialEq, Debug, Clone)]
868#[builder(setter(into))]
869pub struct Extension {
870    /// The schema extension name. For example: dev
871    pub name: String,
872    /// The schema extension unique identifier. For example: 999
873    pub uid: String,
874    /// The schema extension version. For example: 1.0.0-alpha.2
875    pub version: String,
876}
877
878/// `FilteredRequest` provides a mechanism to filter out specific parts of an authorization
879/// decision from being logged within the event.
880#[derive(Default, Debug, Clone, Builder)]
881#[builder(setter(into), default)]
882struct FilteredRequest {
883    pub principal: EntityComponent,
884    pub action: EntityComponent,
885    pub resource: EntityComponent,
886    pub context: Option<String>,
887    pub entities: Option<Entities>,
888}
889
890/// `EntityComponent` typically represents a principal, action or resource within an
891/// authorization decision.
892#[derive(Default, Debug, Clone, PartialEq, Eq)]
893pub(crate) enum EntityComponent {
894    /// A concrete `EntityUID`
895    Concrete(EntityUid),
896    /// An entity that is not specified / concrete.
897    Unspecified,
898    #[default]
899    /// No `EntityUID` because it was filtered out.
900    None,
901}
902
903impl Display for EntityComponent {
904    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
905        match self {
906            Self::Concrete(euid) => {
907                write!(f, "{}", euid.id().escaped())
908            }
909            Self::None => {
910                write!(f, "{SECRET_STRING}")
911            }
912            Self::Unspecified => {
913                write!(f, "*")
914            }
915        }
916    }
917}
918
919impl EntityComponent {
920    /// Gets the component types name.
921    pub fn get_type_name(&self) -> String {
922        match self {
923            Self::Concrete(euid) => euid.type_name().to_string(),
924            Self::None => SECRET_STRING.to_string(),
925            Self::Unspecified => "*".to_string(),
926        }
927    }
928
929    /// Gets the Id of the component.
930    pub fn get_id(&self) -> String {
931        match self {
932            Self::Concrete(euid) => euid.to_string(),
933            Self::None => SECRET_STRING.to_string(),
934            Self::Unspecified => "*".to_string(),
935        }
936    }
937}
938
939impl From<Option<EntityUid>> for EntityComponent {
940    fn from(value: Option<EntityUid>) -> Self {
941        value.map_or_else(|| Self::Unspecified, Self::Concrete)
942    }
943}
944
945#[cfg(test)]
946mod test {
947    use std::collections::{HashMap, HashSet};
948    use std::str::FromStr;
949
950    use cedar_policy::{
951        AuthorizationError, Authorizer, Context, Decision, Entities, EntityId, EntityTypeName,
952        EntityUid, PolicyId, PolicySet, Request, Response,
953    };
954    use serde_json::{from_str, to_string, to_value, Map};
955
956    use crate::public::log::error::OcsfException;
957    use crate::public::log::error::OcsfException::{OcsfFieldsValidationError, UninitializedField};
958    use crate::public::log::schema::{
959        filter_request, ActivityId, EnrichmentArray, EnrichmentArrayBuilder, EntityComponent,
960        ManagedEntity, ManagedEntityBuilder, MetaData, MetaDataBuilder, OpenCyberSecurityFramework,
961        OpenCyberSecurityFrameworkBuilder, ProductBuilder, SeverityId, TypeUid,
962        ALLOWED_ACTIVITY_NAME_LEN, ALLOWED_ENRICHMENT_ARRAY_LEN, OCSF_SCHEMA_VERSION,
963        SECRET_STRING, VENDOR_NAME,
964    };
965    use crate::public::log::{FieldLevel, FieldSet, FieldSetBuilder};
966
967    use super::build_ocsf_severity;
968
969    fn generate_metadata() -> MetaData {
970        MetaDataBuilder::default()
971            .version("1.0.0")
972            .product(
973                ProductBuilder::default()
974                    .vendor_name("cedar-local-agent")
975                    .build()
976                    .unwrap(),
977            )
978            .build()
979            .unwrap()
980    }
981
982    fn generate_entity(entity_type: String, name: String) -> ManagedEntity {
983        ManagedEntityBuilder::default()
984            .version("1.0.0".to_string())
985            .entity_type(entity_type)
986            .name(name)
987            .build()
988            .unwrap()
989    }
990
991    fn generate_default_ocsf_model() -> OpenCyberSecurityFramework {
992        OpenCyberSecurityFrameworkBuilder::default()
993            .type_uid(TypeUid::Read)
994            .severity_id(SeverityId::Unknown)
995            .metadata(generate_metadata())
996            .time(1_695_275_741_i64)
997            .entity(generate_entity("user".to_string(), "alice".to_string()))
998            .activity_id(ActivityId::Read)
999            .build()
1000            .unwrap()
1001    }
1002
1003    fn generate_validation_error() -> Result<OpenCyberSecurityFramework, OcsfException> {
1004        Err(OcsfFieldsValidationError(format!(
1005            "Either the Enrichments array exceeds the maximum allowed size of \
1006	                 {ALLOWED_ENRICHMENT_ARRAY_LEN} elements, or the Activity name exceeds the \
1007	                 maximum allowed length of {ALLOWED_ACTIVITY_NAME_LEN} characters... "
1008        )))
1009    }
1010
1011    fn generate_enrichment_array_vec(num_items: usize) -> Vec<EnrichmentArray> {
1012        (0..num_items).map(|_| EnrichmentArray::default()).collect()
1013    }
1014
1015    fn generate_entity_uid(entity_id: &str) -> EntityUid {
1016        EntityUid::from_type_name_and_id(
1017            EntityTypeName::from_str("CedarLocalAgent::User").unwrap(),
1018            EntityId::from_str(entity_id).unwrap(),
1019        )
1020    }
1021
1022    fn generate_mock_request(principal_name: &str) -> Request {
1023        let principal = generate_entity_uid(principal_name);
1024        let action = generate_entity_uid("read");
1025        let resource = generate_entity_uid("Box");
1026
1027        Request::new(principal, action, resource, Context::empty(), None).unwrap()
1028    }
1029
1030    fn generate_entities() -> Entities {
1031        let entities_data = r#"
1032        [
1033          {
1034            "uid": { "type": "CedarLocalAgent::User", "id": "alice" },
1035            "attrs": {},
1036            "parents": [
1037              { "type": "CedarLocalAgent::UserGroup", "id": "alice_friends" },
1038              { "type": "CedarLocalAgent::UserGroup", "id": "bob_friends" }
1039            ]
1040          },
1041          {
1042            "uid": { "type": "CedarLocalAgent::User", "id": "bob"},
1043            "attrs" : {},
1044            "parents": []
1045          }
1046        ]"#;
1047        Entities::from_json_str(entities_data, None).unwrap()
1048    }
1049
1050    fn generate_custom_count_entities(count: i32) -> Entities {
1051        let mut entities_data = r#"
1052        [
1053          {
1054            "uid": { "type": "CedarLocalAgent::User", "id": "alice" },
1055            "attrs": {},
1056            "parents": [
1057              { "type": "CedarLocalAgent::UserGroup", "id": "alice_friends" },
1058              { "type": "CedarLocalAgent::UserGroup", "id": "bob_friends" }
1059            ]
1060          },
1061          "#
1062        .to_owned();
1063        for i in 0..count {
1064            let append = r#"{
1065                "uid": { "type": "CedarLocalAgent::User", "id": "bob"#
1066                .to_owned()
1067                + i.to_string().as_str()
1068                + r#""},
1069                "attrs" : {},
1070                "parents": []
1071               },"#;
1072
1073            entities_data.push_str(&append);
1074        }
1075        entities_data.pop(); // To remove the final comma (from_json_str throws an error otherwise)
1076        entities_data.push(']');
1077
1078        Entities::from_json_str(&entities_data, None).unwrap()
1079    }
1080
1081    #[allow(clippy::default_trait_access)]
1082    fn generate_response(num_of_error: usize, decision: Decision) -> Response {
1083        let mut policy_ids = HashSet::new();
1084        policy_ids.insert(PolicyId::from_str("policy1").unwrap());
1085        policy_ids.insert(PolicyId::from_str("policy2").unwrap());
1086
1087        let authorizer = Authorizer::new();
1088        let policy_set = PolicySet::from_str(
1089            r"permit(
1090            principal,
1091            action,
1092            resource
1093            ) when {
1094                resource.admins.contains(principal)
1095            };",
1096        )
1097        .unwrap();
1098
1099        let euid_type = EntityTypeName::from_str("Veris::User").unwrap();
1100        let euid_id = EntityId::from_str("test").unwrap();
1101        let euid = EntityUid::from_type_name_and_id(euid_type, euid_id);
1102
1103        let request =
1104            Request::new(euid.clone(), euid.clone(), euid, Context::empty(), None).unwrap();
1105
1106        let auth_res = authorizer.is_authorized(&request, &policy_set, &Entities::empty());
1107        let auth_err = auth_res.diagnostics().errors().next().unwrap();
1108
1109        let errors: Vec<AuthorizationError> = (0..num_of_error).map(|_| auth_err.clone()).collect();
1110
1111        Response::new(decision, policy_ids, errors)
1112    }
1113
1114    #[test]
1115    fn ocsf_field_mapping_allow_case() {
1116        let request = generate_mock_request("alice");
1117        let entities = generate_entities();
1118        let response = generate_response(0, Decision::Allow);
1119        let ocsf = OpenCyberSecurityFramework::create(
1120            &request,
1121            &response,
1122            &entities,
1123            &FieldSet::default(),
1124            "cedar::local::agent::library",
1125        );
1126        assert!(ocsf.is_ok());
1127        let ocsf_log = ocsf.unwrap();
1128        assert_eq!(ocsf_log.severity_id, SeverityId::Informational);
1129        assert_eq!(ocsf_log.status.unwrap(), "Success".to_string());
1130        assert_eq!(ocsf_log.status_code.unwrap(), "Allow".to_string());
1131    }
1132
1133    #[test]
1134    fn ocsf_field_mapping_deny_case() {
1135        let request = generate_mock_request("alice");
1136        let entities = generate_entities();
1137        let response = generate_response(1, Decision::Deny);
1138        let ocsf = OpenCyberSecurityFramework::create(
1139            &request,
1140            &response,
1141            &entities,
1142            &FieldSet::default(),
1143            "cedar::local::agent::library",
1144        );
1145        assert!(ocsf.is_ok());
1146        let ocsf_log = ocsf.unwrap();
1147        assert_eq!(ocsf_log.severity_id, SeverityId::Low);
1148        assert_eq!(ocsf_log.status.unwrap(), "Failure".to_string());
1149
1150        let response = generate_response(2, Decision::Deny);
1151        let ocsf = OpenCyberSecurityFramework::create(
1152            &request,
1153            &response,
1154            &entities,
1155            &FieldSet::default(),
1156            "cedar::local::agent::library",
1157        );
1158
1159        assert!(ocsf.is_ok());
1160        let ocsf_log = ocsf.unwrap();
1161        assert_eq!(ocsf_log.severity_id, SeverityId::Medium);
1162        assert_eq!(ocsf_log.status.unwrap(), "Failure".to_string());
1163        assert_eq!(ocsf_log.status_code.unwrap(), "Deny".to_string());
1164    }
1165
1166    #[test]
1167    fn build_ocsf_severity_multiple_errors() {
1168        assert_eq!(build_ocsf_severity(1), (SeverityId::Low, "Low".to_string()));
1169        assert_eq!(
1170            build_ocsf_severity(4),
1171            (SeverityId::Medium, "Medium".to_string())
1172        );
1173    }
1174
1175    #[test]
1176    fn activity_id_conversion() {
1177        assert_eq!(ActivityId::from("update".to_string()), ActivityId::Update);
1178        assert_eq!(ActivityId::from("delete".to_string()), ActivityId::Delete);
1179        assert_eq!(ActivityId::from("unknown".to_string()), ActivityId::Unknown);
1180        assert_eq!(
1181            ActivityId::from("any_other_activity".to_string()),
1182            ActivityId::Other
1183        );
1184    }
1185
1186    #[test]
1187    fn type_uid_conversion() {
1188        assert_eq!(TypeUid::from(ActivityId::Update), TypeUid::Update);
1189        assert_eq!(TypeUid::from(ActivityId::Delete), TypeUid::Delete);
1190        assert_eq!(TypeUid::from(ActivityId::Unknown), TypeUid::Unknown);
1191        assert_eq!(TypeUid::from(ActivityId::Create), TypeUid::Create);
1192        assert_eq!(TypeUid::from(ActivityId::Other), TypeUid::Other);
1193    }
1194
1195    #[test]
1196    fn ocsf_model_with_property_access_test() {
1197        let ocsf_model = generate_default_ocsf_model();
1198        assert_eq!(ocsf_model.severity_id, SeverityId::Unknown);
1199        assert_eq!(ocsf_model.activity_id, ActivityId::Read);
1200        assert_eq!(
1201            ocsf_model.metadata.product.vendor_name,
1202            "cedar-local-agent".to_string()
1203        );
1204        assert_eq!(
1205            ocsf_model.entity,
1206            generate_entity("user".to_string(), "alice".to_string())
1207        );
1208        assert!(ocsf_model.duration.is_none());
1209    }
1210
1211    #[test]
1212    fn ocsf_test_default() {
1213        let ocsf_model = generate_default_ocsf_model();
1214        println!("{:?}", serde_json::to_string(&ocsf_model).unwrap());
1215        assert_eq!(ocsf_model.class_uid, 3004u64);
1216        assert_eq!(ocsf_model.category_uid, 3u8);
1217    }
1218
1219    #[test]
1220    fn ocsf_test_serialization_and_rename() {
1221        let mut ocsf_model = generate_default_ocsf_model();
1222        ocsf_model.entity.entity_type = Some("Principal".to_string());
1223        let serialized = to_string(&ocsf_model).unwrap();
1224        let deserialized = from_str(&serialized).unwrap();
1225        assert_eq!(ocsf_model, deserialized);
1226        assert!(serialized.contains("\"type\":\"Principal\""));
1227        assert!(serialized.contains("\"activity_id\":2"));
1228    }
1229
1230    #[test]
1231    fn ocsf_test_equality() {
1232        let ocsf_model = generate_default_ocsf_model();
1233        let ocsf_model_2 = generate_default_ocsf_model();
1234        assert_eq!(ocsf_model, ocsf_model_2);
1235    }
1236
1237    #[test]
1238    fn ocsf_test_complex_type() {
1239        let mut ocsf_model = generate_default_ocsf_model();
1240        let mut enrichment_array: HashMap<String, Vec<String>> = HashMap::new();
1241        enrichment_array.insert(
1242            "key1".to_string(),
1243            vec!["value1.1".to_string(), "value1.2".to_string()],
1244        );
1245
1246        ocsf_model.enrichments = Some(Vec::from([EnrichmentArrayBuilder::default()
1247            .name("data1")
1248            .value("value2")
1249            .data(enrichment_array)
1250            .build()
1251            .unwrap()]));
1252        ocsf_model.enrichments.as_ref().map_or_else(
1253            || {
1254                panic!("Enrichment Array is None");
1255            },
1256            |enrichments| {
1257                assert!(!enrichments[0].data.is_empty());
1258                assert_eq!(
1259                    enrichments[0].data["key1"],
1260                    vec!["value1.1".to_string(), "value1.2".to_string()]
1261                );
1262            },
1263        );
1264
1265        let mut unmapped = Map::new();
1266        unmapped.insert("k1".to_string(), to_value("v1").unwrap());
1267        unmapped.insert("k2".to_string(), to_value("v2").unwrap());
1268        let unmapped_obj = to_value(unmapped).unwrap();
1269        ocsf_model.unmapped = Some(unmapped_obj.clone());
1270        assert!(ocsf_model.unmapped.is_some());
1271        assert_eq!(
1272            ocsf_model.unmapped.unwrap().to_string(),
1273            unmapped_obj.to_string()
1274        );
1275    }
1276
1277    #[test]
1278    fn ocsf_validate_required_fields() {
1279        let model_with_no_activity_id = OpenCyberSecurityFrameworkBuilder::default()
1280            .type_uid(TypeUid::Read)
1281            .severity_id(SeverityId::Informational)
1282            .metadata(generate_metadata())
1283            .time(1_695_275_741_i64)
1284            .entity(generate_entity("user".to_string(), "alice".to_string()))
1285            .build();
1286        assert!(model_with_no_activity_id.is_err());
1287        assert!(matches!(
1288            model_with_no_activity_id,
1289            Err(UninitializedField(_))
1290        ));
1291    }
1292
1293    #[test]
1294    fn ocsf_validate_activity_name() {
1295        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1296            .type_uid(TypeUid::Read)
1297            .severity_id(SeverityId::Unknown)
1298            .metadata(generate_metadata())
1299            .time(1_695_275_741_i64)
1300            .entity(generate_entity("user".to_string(), "alice".to_string()))
1301            .activity_id(ActivityId::Read)
1302            .enrichments(generate_enrichment_array_vec(1))
1303            .activity_name(
1304                "this is an invalid activity name with \
1305            len larger than 35"
1306                    .to_string(),
1307            )
1308            .build();
1309        assert!(log_result.is_err());
1310        let _expected = generate_validation_error();
1311        assert!(matches!(log_result, _expected));
1312    }
1313
1314    #[test]
1315    fn ocsf_validate_activity_name_enrichment_none() {
1316        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1317            .type_uid(TypeUid::Read)
1318            .severity_id(SeverityId::Unknown)
1319            .metadata(generate_metadata())
1320            .time(1_695_275_741_i64)
1321            .entity(generate_entity("user".to_string(), "alice".to_string()))
1322            .activity_id(ActivityId::Read)
1323            .activity_name(
1324                "this is an invalid activity name with \
1325            len larger than 35"
1326                    .to_string(),
1327            )
1328            .build();
1329        assert!(log_result.is_err());
1330        let _expected = generate_validation_error();
1331        assert!(matches!(log_result, _expected));
1332    }
1333
1334    #[test]
1335    fn ocsf_validate_activity_name_none() {
1336        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1337            .type_uid(TypeUid::Read)
1338            .severity_id(SeverityId::Unknown)
1339            .metadata(generate_metadata())
1340            .time(1_695_275_741_i64)
1341            .entity(generate_entity("user".to_string(), "alice".to_string()))
1342            .activity_id(ActivityId::Read)
1343            .enrichments(generate_enrichment_array_vec(1))
1344            .activity_name(None)
1345            .build();
1346        assert!(log_result.is_ok());
1347    }
1348
1349    #[test]
1350    fn ocsf_validate_activity_enrichments() {
1351        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1352            .type_uid(TypeUid::Read)
1353            .severity_id(SeverityId::Unknown)
1354            .metadata(generate_metadata())
1355            .time(1_695_275_741_i64)
1356            .entity(generate_entity("user".to_string(), "alice".to_string()))
1357            .activity_id(ActivityId::Read)
1358            .enrichments(generate_enrichment_array_vec(10))
1359            .activity_name("safe-string".to_string())
1360            .build();
1361        assert!(log_result.is_err());
1362        let _expected = generate_validation_error();
1363        assert!(matches!(log_result, _expected));
1364    }
1365
1366    /// This test proves that user request input is being redacted on `FieldSet::default()`
1367    #[test]
1368    fn validate_user_input_no_effect_on_log_size() {
1369        let response = generate_response(0, Decision::Allow);
1370        let fields = FieldSet::default();
1371        let authorizer_name = "cedar::local::agent::library";
1372
1373        let request_json_1 = {
1374            let request = generate_mock_request("alice111");
1375            let entities = generate_custom_count_entities(100);
1376
1377            let ocsf = OpenCyberSecurityFramework::create(
1378                &request,
1379                &response,
1380                &entities,
1381                &fields,
1382                authorizer_name,
1383            );
1384
1385            serde_json::to_string(&ocsf.unwrap()).unwrap()
1386        };
1387
1388        assert!(!request_json_1.contains("alice111"));
1389        assert!(!request_json_1.contains("bob"));
1390
1391        let request_json_2 = {
1392            let request = generate_mock_request("alice");
1393            let entities = generate_custom_count_entities(50);
1394            let ocsf = OpenCyberSecurityFramework::create(
1395                &request,
1396                &response,
1397                &entities,
1398                &fields,
1399                authorizer_name,
1400            );
1401
1402            serde_json::to_string(&ocsf.unwrap()).unwrap()
1403        };
1404
1405        assert!(!request_json_2.contains("alice"));
1406        assert!(!request_json_2.contains("bob"));
1407
1408        assert_eq!(request_json_1.len(), request_json_2.len());
1409    }
1410
1411    #[test]
1412    fn ocsf_validate_activity_enrichments_activity_name_none() {
1413        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1414            .type_uid(TypeUid::Read)
1415            .severity_id(SeverityId::Unknown)
1416            .metadata(generate_metadata())
1417            .time(1_695_275_741_i64)
1418            .entity(generate_entity("user".to_string(), "alice".to_string()))
1419            .activity_id(ActivityId::Read)
1420            .enrichments(generate_enrichment_array_vec(10))
1421            .build();
1422        let _expected = generate_validation_error();
1423        assert!(matches!(log_result, _expected));
1424    }
1425
1426    #[test]
1427    fn ocsf_validate_activity_enrichments_none() {
1428        let log_result = OpenCyberSecurityFrameworkBuilder::default()
1429            .type_uid(TypeUid::Read)
1430            .severity_id(SeverityId::Unknown)
1431            .metadata(generate_metadata())
1432            .time(1_695_275_741_i64)
1433            .entity(generate_entity("user".to_string(), "alice".to_string()))
1434            .activity_id(ActivityId::Read)
1435            .enrichments(None)
1436            .activity_name("safe-string".to_string())
1437            .build();
1438        assert!(log_result.is_ok());
1439    }
1440
1441    fn create_mock_request() -> Request {
1442        let principal = EntityUid::from_type_name_and_id(
1443            EntityTypeName::from_str("User").unwrap(),
1444            EntityId::from_str("Alice").unwrap(),
1445        );
1446        let action = EntityUid::from_type_name_and_id(
1447            EntityTypeName::from_str("Action").unwrap(),
1448            EntityId::from_str("Read").unwrap(),
1449        );
1450        let resource = EntityUid::from_type_name_and_id(
1451            EntityTypeName::from_str("Photo").unwrap(),
1452            EntityId::from_str("vacation.jpg").unwrap(),
1453        );
1454        Request::new(principal, action, resource, Context::empty(), None).unwrap()
1455    }
1456
1457    fn create_mock_entities() -> Entities {
1458        let entities_data = r#"
1459	         [
1460	           {
1461	             "uid": { "type": "User", "id": "Alice" },
1462	             "attrs": {},
1463	             "parents": []
1464	           },
1465	           {
1466	             "uid": { "type": "User", "id": "Bob"},
1467	             "attrs" : {},
1468	             "parents": []
1469	           }
1470	         ]"#;
1471        Entities::from_json_str(entities_data, None).unwrap()
1472    }
1473
1474    #[test]
1475    fn filter_request_default_field_set() {
1476        let request = create_mock_request();
1477        let entities = create_mock_entities();
1478        let field_set = FieldSetBuilder::default().build().unwrap();
1479        let filtered_request = filter_request(&request, &entities, &field_set);
1480
1481        assert_eq!(filtered_request.principal, EntityComponent::None);
1482        assert_eq!(filtered_request.action, EntityComponent::None);
1483        assert_eq!(filtered_request.resource, EntityComponent::None);
1484        assert!(filtered_request.context.is_none());
1485        assert!(filtered_request.entities.is_none());
1486    }
1487
1488    #[test]
1489    fn filter_request_all_fields_set() {
1490        let request = create_mock_request();
1491        let entities = create_mock_entities();
1492        let field_set = FieldSetBuilder::default()
1493            .principal(true)
1494            .action(true)
1495            .resource(true)
1496            .context(true)
1497            .entities(FieldLevel::All)
1498            .build()
1499            .unwrap();
1500        let filtered_request = filter_request(&request, &entities, &field_set);
1501
1502        assert!(matches!(
1503            filtered_request.principal,
1504            EntityComponent::Concrete(_)
1505        ));
1506        assert!(matches!(
1507            filtered_request.action,
1508            EntityComponent::Concrete(_)
1509        ));
1510        assert!(matches!(
1511            filtered_request.resource,
1512            EntityComponent::Concrete(_)
1513        ));
1514        assert!(filtered_request.context.is_some());
1515        assert!(filtered_request.entities.is_some());
1516    }
1517
1518    #[test]
1519    fn filter_request_custom_field_set() {
1520        let request = create_mock_request();
1521        let entities = create_mock_entities();
1522        let filter_fn = |_entities: &Entities| -> Entities { Entities::empty() };
1523        let field_set = FieldSetBuilder::default()
1524            .principal(true)
1525            .context(true)
1526            .entities(FieldLevel::Custom(filter_fn))
1527            .build()
1528            .unwrap();
1529
1530        let filtered_request = filter_request(&request, &entities, &field_set);
1531
1532        assert!(matches!(
1533            filtered_request.principal,
1534            EntityComponent::Concrete(_)
1535        ));
1536        assert!(matches!(filtered_request.action, EntityComponent::None));
1537        assert!(matches!(filtered_request.resource, EntityComponent::None));
1538
1539        assert_eq!(filtered_request.context, Some(request.to_string()));
1540        assert_eq!(filtered_request.entities, Some(Entities::empty()));
1541    }
1542
1543    #[test]
1544    fn ocsf_error_log() {
1545        let ocsf = OpenCyberSecurityFramework::error(
1546            "Failed to create error".to_string(),
1547            "some_authorizer".to_string(),
1548        );
1549
1550        assert_eq!(ocsf.type_uid, TypeUid::Other);
1551        assert_eq!(ocsf.severity_id, SeverityId::Other);
1552        assert_eq!(ocsf.activity_id, ActivityId::Other);
1553        assert_eq!(
1554            ocsf.entity,
1555            ManagedEntityBuilder::default()
1556                .name("N/A".to_string())
1557                .build()
1558                .unwrap()
1559        );
1560        assert_eq!(ocsf.message.unwrap(), "Failed to create error".to_string());
1561        assert_eq!(
1562            ocsf.metadata,
1563            MetaDataBuilder::default()
1564                .version(OCSF_SCHEMA_VERSION)
1565                .product(
1566                    ProductBuilder::default()
1567                        .vendor_name(VENDOR_NAME)
1568                        .name("some_authorizer".to_string())
1569                        .build()
1570                        .unwrap()
1571                )
1572                .build()
1573                .unwrap()
1574        );
1575    }
1576
1577    #[test]
1578    fn display_entity_component_concrete() {
1579        let component = EntityComponent::Concrete(EntityUid::from_str("Action::\"test\"").unwrap());
1580        assert_eq!("test", component.to_string());
1581    }
1582
1583    #[test]
1584    fn display_entity_component_unspecified() {
1585        let component = EntityComponent::Unspecified;
1586        assert_eq!("*", component.to_string());
1587        assert_eq!("*", component.get_id());
1588        assert_eq!("*", component.get_type_name());
1589    }
1590
1591    #[test]
1592    fn display_entity_component_filtered_out() {
1593        let component = EntityComponent::None;
1594        assert_eq!(SECRET_STRING.to_string(), component.to_string());
1595        assert_eq!(SECRET_STRING.to_string(), component.get_id());
1596        assert_eq!(SECRET_STRING.to_string(), component.get_type_name());
1597    }
1598}