Skip to content

Commit 03ea5d8

Browse files
committed
fix(mcp): include tool names in policy logs
Signed-off-by: Kirit93 <kthadaka@nvidia.com>
1 parent ff9af8e commit 03ea5d8

2 files changed

Lines changed: 113 additions & 4 deletions

File tree

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,19 @@ impl OcsfEvent {
218218
(false, true) => format!(" {action}"),
219219
(false, false) => format!(" {action}{arrow}"),
220220
};
221-
format!("HTTP:{method} {sev}{detail}{rule_ctx}{reason_ctx}")
221+
// Denied HTTP events surface their message through reason_ctx.
222+
// Allowed MCP decisions also need the JSON-RPC message so the
223+
// selected tool name is visible in the shorthand log stream.
224+
let message_ctx = if action != "DENIED"
225+
&& e.firewall_rule
226+
.as_ref()
227+
.is_some_and(|rule| rule.rule_type == "l7-mcp")
228+
{
229+
message_tag(&e.base)
230+
} else {
231+
String::new()
232+
};
233+
format!("HTTP:{method} {sev}{detail}{rule_ctx}{reason_ctx}{message_ctx}")
222234
}
223235

224236
Self::SshActivity(e) => {
@@ -534,6 +546,62 @@ mod tests {
534546
);
535547
}
536548

549+
#[test]
550+
fn test_http_activity_shorthand_mcp_shows_tool_for_allow_and_deny() {
551+
let mut event_base = base(4002, "HTTP Activity", 4, "Network Activity", 0, "Other");
552+
event_base.set_message(
553+
"JSONRPC_L7_REQUEST decision=allow rule_methods=tools/call tools=move_head",
554+
);
555+
let event = OcsfEvent::HttpActivity(HttpActivityEvent {
556+
base: event_base,
557+
http_request: Some(HttpRequest::new(
558+
"POST",
559+
Url::new("http", "host.openshell.internal", "/mcp", 8766),
560+
)),
561+
http_response: None,
562+
src_endpoint: None,
563+
dst_endpoint: None,
564+
proxy_endpoint: None,
565+
actor: None,
566+
firewall_rule: Some(FirewallRule::new("reachy-mcp", "l7-mcp")),
567+
action: Some(ActionId::Allowed),
568+
disposition: Some(DispositionId::Allowed),
569+
observation_point_id: None,
570+
is_src_dst_assignment_known: None,
571+
});
572+
573+
let shorthand = event.format_shorthand();
574+
assert!(shorthand.contains("engine:l7-mcp"));
575+
assert!(shorthand.contains("tools=move_head"));
576+
577+
let mut denied_base = base(4002, "HTTP Activity", 4, "Network Activity", 0, "Other");
578+
denied_base.severity = crate::enums::SeverityId::Medium;
579+
denied_base.set_message(
580+
"JSONRPC_L7_REQUEST decision=deny rule_methods=tools/call tools=move_head",
581+
);
582+
let denied_event = OcsfEvent::HttpActivity(HttpActivityEvent {
583+
base: denied_base,
584+
http_request: Some(HttpRequest::new(
585+
"POST",
586+
Url::new("http", "host.openshell.internal", "/mcp", 8766),
587+
)),
588+
http_response: None,
589+
src_endpoint: None,
590+
dst_endpoint: None,
591+
proxy_endpoint: None,
592+
actor: None,
593+
firewall_rule: Some(FirewallRule::new("reachy-mcp", "l7-mcp")),
594+
action: Some(ActionId::Denied),
595+
disposition: Some(DispositionId::Blocked),
596+
observation_point_id: None,
597+
is_src_dst_assignment_known: None,
598+
});
599+
600+
let denied_shorthand = denied_event.format_shorthand();
601+
assert!(denied_shorthand.contains("[reason:"));
602+
assert!(denied_shorthand.contains("tools=move_head"));
603+
}
604+
537605
#[test]
538606
fn test_network_activity_shorthand_denied_shows_reason() {
539607
let mut b = base(4001, "Network Activity", 4, "Network Activity", 1, "Open");

crates/openshell-supervisor-network/src/l7/relay.rs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,11 @@ fn l7_protocol_log_summary(
571571
}
572572

573573
if let Some(info) = jsonrpc_info {
574-
return format!(" rule_methods={}", rule_method_names_for_log(info));
574+
return format!(
575+
" rule_methods={} tools={}",
576+
rule_method_names_for_log(info),
577+
tool_names_for_log(info)
578+
);
575579
}
576580

577581
String::new()
@@ -1428,8 +1432,9 @@ pub(crate) fn jsonrpc_log_message(
14281432
reason: &str,
14291433
) -> String {
14301434
let rule_methods = rule_method_names_for_log(info);
1435+
let tools = tool_names_for_log(info);
14311436
format!(
1432-
"JSONRPC_L7_REQUEST decision={decision} http_method={http_method} endpoint={endpoint} rule_methods={rule_methods} policy_version={policy_version} reason={reason}"
1437+
"JSONRPC_L7_REQUEST decision={decision} rule_methods={rule_methods} tools={tools} http_method={http_method} endpoint={endpoint} policy_version={policy_version} reason={reason}"
14331438
)
14341439
}
14351440

@@ -1444,6 +1449,20 @@ pub(crate) fn rule_method_names_for_log(info: &crate::l7::jsonrpc::JsonRpcReques
14441449
.join(",")
14451450
}
14461451

1452+
pub(crate) fn tool_names_for_log(info: &crate::l7::jsonrpc::JsonRpcRequestInfo) -> String {
1453+
let tools = info
1454+
.calls
1455+
.iter()
1456+
.filter_map(|call| call.tool.as_deref())
1457+
.map(sanitize_log_token)
1458+
.collect::<Vec<_>>();
1459+
if tools.is_empty() {
1460+
"-".to_string()
1461+
} else {
1462+
tools.join(",")
1463+
}
1464+
}
1465+
14471466
fn sanitize_log_token(value: &str) -> String {
14481467
value
14491468
.chars()
@@ -2538,7 +2557,6 @@ network_policies:
25382557

25392558
let (allowed, reason) = evaluate_l7_request(&tunnel_engine, &ctx, &request).unwrap();
25402559
assert!(allowed, "{reason}");
2541-
25422560
request.jsonrpc = Some(crate::l7::jsonrpc::parse_jsonrpc_body(
25432561
br#"{"jsonrpc":"2.0","id":1,"method":"reports.search","params":["ignored",{"nested":true}]}"#,
25442562
crate::l7::jsonrpc::JsonRpcInspectionMode::JsonRpc,
@@ -2604,6 +2622,17 @@ network_policies:
26042622

26052623
let (allowed, reason) = evaluate_l7_request(&tunnel_engine, &ctx, &request).unwrap();
26062624
assert!(allowed, "{reason}");
2625+
let allowed_info = request.jsonrpc.as_ref().expect("parsed MCP request");
2626+
let allowed_message = jsonrpc_log_message(
2627+
"allow",
2628+
"POST",
2629+
"api.example.test:443/mcp",
2630+
allowed_info,
2631+
42,
2632+
&reason,
2633+
);
2634+
assert!(allowed_message.contains("rule_methods=tools/call"));
2635+
assert!(allowed_message.contains("tools=read_status"));
26072636

26082637
request.jsonrpc = Some(crate::l7::jsonrpc::parse_jsonrpc_body(
26092638
br#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_resource","arguments":{"scope":"workspace/main"}}}"#,
@@ -2625,6 +2654,17 @@ network_policies:
26252654
reason.contains("deny rule"),
26262655
"deny reason should identify policy denial: {reason}"
26272656
);
2657+
let denied_message = jsonrpc_log_message(
2658+
"deny",
2659+
"POST",
2660+
"api.example.test:443/mcp",
2661+
parsed,
2662+
42,
2663+
&reason,
2664+
);
2665+
assert!(denied_message.contains("rule_methods=tools/call"));
2666+
assert!(denied_message.contains("tools=delete_resource"));
2667+
assert!(!denied_message.contains("workspace/main"));
26282668
}
26292669

26302670
#[test]
@@ -2644,6 +2684,7 @@ network_policies:
26442684

26452685
assert!(message.contains("endpoint=jsonrpc.example.com:443/rpc"));
26462686
assert!(message.contains("rule_methods=reports.archive"));
2687+
assert!(message.contains("tools=-"));
26472688
assert!(message.contains("policy_version=42"));
26482689
assert!(!message.contains("delete_resource"));
26492690
assert!(!message.contains("secret-scope"));

0 commit comments

Comments
 (0)