Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ version section and adds the squash-merged PR titles since the previous tag. Ver
## [Unreleased]

- chore(deps): build against OpenShell v0.1.7 (`openshell-policy-schema`, `openshell-policy`); docs and generated policy headers now name v0.1.7
- fix(diff-logs): ignore OCSF Network Activity Refuse records (policy DNS `policy_dns_ineligible`) that have no port, so OCSF JSON and shorthand logs report the same denials

## [0.1.0-rc.3] - 2026-11-09

Expand Down
39 changes: 39 additions & 0 deletions src/diff_logs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,21 @@ fn strip_query(p: &str) -> String {
/// Only network activity (class 4001; activity 4 is a failure) and HTTP activity (class 4002) are used. Returns `None`
/// for other classes, records without a destination host or an action, and the binary is dropped unless it is an
/// absolute path.
///
/// Network "Refuse" records (activity 5, e.g. `status_detail: policy_dns_ineligible` when policy DNS refuses a name the
/// policy does not allow) are skipped as well: they carry no port or process, and the connection attempt that follows
/// is logged separately as a denied Open with host, port and binary. The shorthand parser ignores `NET:REFUSE` too.
fn parse_json_line(v: &serde_json::Value, source: &str, line: usize) -> Option<Event> {
let class_uid = v.get("class_uid")?.as_u64()?;
if class_uid != 4001 && class_uid != 4002 {
return None;
}
if class_uid == 4001
&& (v.get("activity_id").and_then(|a| a.as_u64()) == Some(5)
|| v.get("status_detail").and_then(|d| d.as_str()) == Some("policy_dns_ineligible"))
{
return None;
}
let failed = class_uid == 4001 && v.get("activity_id").and_then(|a| a.as_u64()) == Some(4);
let action = match v.get("action").and_then(|a| a.as_str()) {
_ if failed => Action::Failed,
Expand Down Expand Up @@ -949,6 +959,35 @@ garbage line\n";
);
}

#[test]
fn skips_ocsf_network_refuse_records() {
// Real OpenShell v0.1.7 (OCSF 1.8.0) policy-DNS refusal followed by the denied connection it precedes.
let t = concat!(
"{\"class_uid\":4001,\"activity_id\":5,\"activity_name\":\"Refuse\",\"status_detail\":\"policy_dns_ineligible\",",
"\"dst_endpoint\":{\"domain\":\"example.com\"},\"action_id\":2,\"action\":\"Denied\",\"metadata\":{\"version\":\"1.8.0\"}}\n",
"{\"class_uid\":4001,\"activity_id\":1,\"status_detail\":\"policy_dns_ineligible\",\"dst_endpoint\":{\"domain\":\"b.example\"},\"action\":\"Denied\"}\n",
"{\"class_uid\":4001,\"activity_id\":1,\"status_detail\":\"transparent_tcp_policy_denied\",",
"\"dst_endpoint\":{\"domain\":\"example.com\",\"ip\":\"198.18.0.3\",\"port\":443},",
"\"actor\":{\"process\":{\"name\":\"/usr/bin/curl\",\"pid\":0}},\"action_id\":2,\"action\":\"Denied\",\"metadata\":{\"version\":\"1.8.0\"}}\n",
);
let e = parse_logs(t, "x");
assert_eq!(e.len(), 1);
assert_eq!(
(
e[0].action,
e[0].host.as_str(),
e[0].port,
e[0].binary.as_deref()
),
(
Action::Denied,
"example.com",
Some(443),
Some("/usr/bin/curl")
)
);
}

#[test]
fn git_helpers_match_their_parent_binary() {
assert!(bin_matches(
Expand Down
Loading