Skip to content

Commit c4b0dcf

Browse files
committed
feat(ocsf): enrich middleware shorthand logs
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
1 parent 59725ee commit c4b0dcf

2 files changed

Lines changed: 125 additions & 13 deletions

File tree

crates/openshell-ocsf/src/format/shorthand.rs

Lines changed: 117 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,22 @@
77
88
use crate::events::OcsfEvent;
99
use crate::events::base_event::BaseEventData;
10-
use crate::objects::Url;
10+
use crate::objects::{Evidence, Url};
11+
12+
fn finding_evidence_value<'a>(evidences: Option<&'a [Evidence]>, key: &str) -> Option<&'a str> {
13+
evidences?
14+
.iter()
15+
.filter_map(|evidence| evidence.data.as_ref()?.as_object())
16+
.find_map(|data| data.get(key)?.as_str())
17+
}
18+
19+
fn message_bool_value<'a>(message: Option<&'a str>, key: &str) -> Option<&'a str> {
20+
let prefix = format!("{key}=");
21+
message?
22+
.split_ascii_whitespace()
23+
.find_map(|field| field.strip_prefix(&prefix))
24+
.filter(|value| matches!(*value, "true" | "false"))
25+
}
1126

1227
/// Format a timestamp (ms since epoch) as `HH:MM:SS.mmm`.
1328
///
@@ -195,10 +210,32 @@ impl OcsfEvent {
195210
.and_then(|r| r.url.as_ref())
196211
.map(Url::to_display_string)
197212
.unwrap_or_default();
213+
let transformed = e
214+
.firewall_rule
215+
.as_ref()
216+
.filter(|rule| rule.rule_type == "middleware")
217+
.and_then(|_| message_bool_value(e.base.message.as_deref(), "transformed"));
218+
let failed = e
219+
.firewall_rule
220+
.as_ref()
221+
.filter(|rule| rule.rule_type == "middleware")
222+
.and_then(|_| message_bool_value(e.base.message.as_deref(), "failed"));
198223
let rule_ctx = e
199224
.firewall_rule
200225
.as_ref()
201-
.map(|r| format!(" [policy:{} engine:{}]", r.name, r.rule_type))
226+
.map(|r| {
227+
let mut context = vec![
228+
format!("policy:{}", r.name),
229+
format!("engine:{}", r.rule_type),
230+
];
231+
if let Some(value) = transformed {
232+
context.push(format!("transformed:{value}"));
233+
}
234+
if let Some(value) = failed {
235+
context.push(format!("failed:{value}"));
236+
}
237+
format!(" [{}]", context.join(" "))
238+
})
202239
.unwrap_or_default();
203240
// For denied events, surface the reason from status_detail
204241
let reason_ctx = if action == "DENIED" {
@@ -280,16 +317,26 @@ impl OcsfEvent {
280317
}
281318

282319
Self::DetectionFinding(e) => {
283-
let disposition = e
284-
.disposition
285-
.map_or_else(|| "UNKNOWN".to_string(), |d| d.label().to_uppercase());
320+
let disposition = e.disposition.map_or_else(
321+
|| e.base.activity_name.to_uppercase(),
322+
|d| d.label().to_uppercase(),
323+
);
286324
let title = &e.finding_info.title;
287-
let confidence_ctx = e
288-
.confidence
289-
.map(|c| format!(" [confidence:{}]", c.label().to_lowercase()))
290-
.unwrap_or_default();
291-
292-
format!("FINDING:{disposition} {sev} \"{title}\"{confidence_ctx}")
325+
let mut context = vec![format!("type:{}", e.finding_info.uid)];
326+
if let Some(middleware) =
327+
finding_evidence_value(e.evidences.as_deref(), "middleware")
328+
{
329+
context.push(format!("middleware:{middleware}"));
330+
}
331+
if let Some(count) = finding_evidence_value(e.evidences.as_deref(), "count") {
332+
context.push(format!("count:{count}"));
333+
}
334+
if let Some(confidence) = e.confidence {
335+
context.push(format!("confidence:{}", confidence.label().to_lowercase()));
336+
}
337+
let context = format!(" [{}]", context.join(" "));
338+
339+
format!("FINDING:{disposition} {sev} \"{title}\"{context}")
293340
}
294341

295342
Self::ApplicationLifecycle(e) => {
@@ -534,6 +581,37 @@ mod tests {
534581
);
535582
}
536583

584+
#[test]
585+
fn test_http_activity_shorthand_includes_middleware_outcome() {
586+
let mut base = base(4002, "HTTP Activity", 4, "Network Activity", 99, "Other");
587+
base.set_message(
588+
"MIDDLEWARE prototype-content-guard example/content-guard decision=Allow transformed=false failed=true",
589+
);
590+
let event = OcsfEvent::HttpActivity(HttpActivityEvent {
591+
base,
592+
http_request: Some(HttpRequest::new(
593+
"POST",
594+
Url::new("http", "httpbin.org", "/anything", 443),
595+
)),
596+
http_response: None,
597+
src_endpoint: None,
598+
dst_endpoint: None,
599+
proxy_endpoint: None,
600+
actor: None,
601+
firewall_rule: Some(FirewallRule::new("httpbin", "middleware")),
602+
action: Some(ActionId::Allowed),
603+
disposition: Some(DispositionId::Allowed),
604+
observation_point_id: None,
605+
is_src_dst_assignment_known: None,
606+
});
607+
608+
let shorthand = event.format_shorthand();
609+
assert_eq!(
610+
shorthand,
611+
"HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware transformed:false failed:true]"
612+
);
613+
}
614+
537615
#[test]
538616
fn test_network_activity_shorthand_denied_shows_reason() {
539617
let mut b = base(4001, "Network Activity", 4, "Network Activity", 1, "Open");
@@ -863,8 +941,35 @@ mod tests {
863941
let shorthand = event.format_shorthand();
864942
assert_eq!(
865943
shorthand,
866-
"FINDING:BLOCKED [HIGH] \"NSSH1 Nonce Replay Attack\" [confidence:high]"
944+
"FINDING:BLOCKED [HIGH] \"NSSH1 Nonce Replay Attack\" [type:nssh1-replay-abc confidence:high]"
945+
);
946+
}
947+
948+
#[test]
949+
fn test_detection_finding_shorthand_uses_activity_and_safe_evidence() {
950+
let event = OcsfEvent::DetectionFinding(DetectionFindingEvent {
951+
base: base(2004, "Detection Finding", 2, "Findings", 1, "Create"),
952+
finding_info: FindingInfo::new("content_guard.match", "configured content matched"),
953+
evidences: Some(vec![Evidence::from_pairs(&[
954+
("middleware", "prototype-content-guard"),
955+
("count", "1"),
956+
("matched_content", "must-not-appear"),
957+
])]),
958+
attacks: None,
959+
remediation: None,
960+
is_alert: None,
961+
confidence: None,
962+
risk_level: None,
963+
action: None,
964+
disposition: None,
965+
});
966+
967+
let shorthand = event.format_shorthand();
968+
assert_eq!(
969+
shorthand,
970+
"FINDING:CREATE [INFO] \"configured content matched\" [type:content_guard.match middleware:prototype-content-guard count:1]"
867971
);
972+
assert!(!shorthand.contains("must-not-appear"));
868973
}
869974

870975
#[test]

docs/observability/logging.mdx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ CLASS:ACTIVITY [SEVERITY] ACTION DETAILS [CONTEXT]
9494
- SSH: peer address and authentication type
9595
- Process: `name(pid)` with exit code or command line
9696
- Config: description of what changed
97-
- Finding: quoted title with confidence level
97+
- Finding: quoted title with the stable finding type, optional confidence, and allowlisted evidence fields when available
9898

9999
**Context** in brackets at the end provides the policy rule and enforcement engine that produced the decision.
100100

@@ -130,6 +130,13 @@ An HTTP request to a non-default port. HTTP log URLs include the port whenever i
130130
OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy:internal_api engine:opa]
131131
```
132132

133+
A supervisor middleware HTTP event records whether it transformed the request. If the middleware also emits a finding, that remains a separate event:
134+
135+
```text
136+
OCSF HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware transformed:true failed:false]
137+
OCSF FINDING:CREATE [MED] "configured content matched" [type:content_guard.match middleware:prototype-content-guard count:1]
138+
```
139+
133140
Proxy and SSH servers ready:
134141

135142
```text

0 commit comments

Comments
 (0)