Skip to content
Open
59 changes: 53 additions & 6 deletions crates/openshell-supervisor-network/src/l7/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,11 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
};

for (i, ep) in endpoints.iter().enumerate() {
let loc = format!("{name}.endpoints[{i}]");
if !ep.is_object() {
errors.push(format!("{loc}: endpoint entry must be an object"));
continue;
}
let protocol = ep.get("protocol").and_then(|v| v.as_str()).unwrap_or("");
let l7_protocol = L7Protocol::parse(protocol);
let jsonrpc_family = l7_protocol.is_some_and(L7Protocol::is_jsonrpc_family);
Expand All @@ -986,9 +991,13 @@ pub fn validate_l7_policies(data_json: &serde_json::Value) -> (Vec<String>, Vec<
.into_iter()
.collect()
},
|arr| arr.iter().filter_map(serde_json::Value::as_u64).collect(),
|arr| {
arr.iter()
.filter_map(serde_json::Value::as_u64)
.filter(|p| *p > 0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning: This filters only a temporary vector whose sole consumer already checks any(|port| *port > 0). Consequently, [0] was already rejected, while [0, 443] still passes and the zero remains in the JSON supplied to OPA. Please either reject any zero-valued ports member or remove zeros during endpoint normalization/proto serialization, then cover all-zero and mixed arrays with regression tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will get to these later today thanks for raising this issue.

.collect()
},
);
let loc = format!("{name}.endpoints[{i}]");

if protocol == "mcp" {
if host.trim().is_empty() {
Expand Down Expand Up @@ -1557,7 +1566,13 @@ pub fn expand_access_presets(data: &mut serde_json::Value) -> Vec<String> {
&& !has_rules
&& mcp_allow_all_known_mcp_methods
{
ep.as_object_mut().unwrap().insert(
let Some(obj) = ep.as_object_mut() else {
warnings.push(format!(
"{name}.endpoints[{i}]: endpoint entry is not an object; skipping access preset expansion"
));
continue;
};
obj.insert(
"rules".to_string(),
serde_json::Value::Array(vec![jsonrpc_rule_json("*")]),
);
Expand All @@ -1582,9 +1597,13 @@ pub fn expand_access_presets(data: &mut serde_json::Value) -> Vec<String> {
continue;
};

ep.as_object_mut()
.unwrap()
.insert("rules".to_string(), serde_json::Value::Array(rules));
if let Some(obj) = ep.as_object_mut() {
obj.insert("rules".to_string(), serde_json::Value::Array(rules));
} else {
warnings.push(format!(
"{name}.endpoints[{i}]: endpoint entry is not an object; skipping access preset expansion"
));
}
}
}

Expand Down Expand Up @@ -1629,6 +1648,34 @@ fn graphql_rule_json(operation_type: &str) -> serde_json::Value {
mod tests {
use super::*;

#[test]
fn validate_l7_policies_rejects_non_object_endpoint() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Suggestion: This test verifies validation, but not expand_access_presets, where the unwraps were removed. Please add a direct expansion test containing a non-object endpoint and a valid preset endpoint, asserting the invalid entry remains unchanged and the valid preset expands. It would also be stronger to assert indexed errors for both malformed entries.

let data = serde_json::json!({
"network_policies": {
"test": {
"endpoints": [
"not-an-object",
{"host": "api.example.com", "port": 443, "protocol": "rest"},
42,
],
"binaries": []
}
}
});
let (errors, _warnings) = validate_l7_policies(&data);
assert!(
errors
.iter()
.any(|e| e.contains("endpoint entry must be an object")),
"expected non-object endpoint error: {errors:?}"
);
// The valid object endpoint should not produce an error.
assert!(
!errors.iter().any(|e| e.contains("api.example.com")),
"valid endpoint should not be blamed: {errors:?}"
);
}

#[test]
fn parse_l7_config_rest_enforce() {
let val = regorus::Value::from_json_str(
Expand Down
84 changes: 83 additions & 1 deletion crates/openshell-supervisor-network/src/opa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1118,7 +1118,13 @@ fn normalize_endpoint_ports(data: &mut serde_json::Value) {
continue;
};

// If "ports" already exists and is non-empty, keep it.
// If "ports" already exists, filter out zero values so OPA never
// sees a zero port. An all-zero array becomes empty and falls back
// to scalar "port" promotion below.
if let Some(ports) = ep_obj.get_mut("ports").and_then(|v| v.as_array_mut()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Critical (CWE-20): This normalizer runs only from preprocess_yaml_data; production OpaEngine::from_proto loads proto_to_opa_data_json without calling it. validate_l7_policies filters only a temporary vector, so a CLI policy with ports: [0, 443] still reaches OPA containing zero during normal sandbox load/reload. Please filter in proto_to_opa_data_json or apply shared normalization to proto-generated JSON, then add an OpaEngine::from_proto regression test.

ports.retain(|p| p.as_u64().is_some_and(|n| n > 0));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-20): retain removes every value that is not a positive JSON u64, not only zero. In local-file mode, ports: ["443"] plus port: 8443 therefore becomes ports: [8443], silently converting malformed authorization input into an allow. Please reject non-integer entries before normalization, or remove exactly numeric zero (p.as_u64() != Some(0)) so malformed arrays remain fail-closed. Add a regression case.

}

let has_ports = ep_obj
.get("ports")
.and_then(|v| v.as_array())
Expand Down Expand Up @@ -7705,4 +7711,80 @@ network_policies:
cmdline_paths: vec![],
}
}

#[test]
fn normalize_endpoint_ports_filters_zero_values() {
let mut data = serde_json::json!({
"network_policies": {
"p": {
"endpoints": [
{"host": "h1.test", "ports": [0, 443]},
{"host": "h2.test", "ports": [0]},
{"host": "h3.test", "port": 0},
{"host": "h4.test", "port": 8080},
]
}
}
});
normalize_endpoint_ports(&mut data);
let endpoints = data["network_policies"]["p"]["endpoints"]
.as_array()
.unwrap();

// Mixed array: zero removed, positive kept.
assert_eq!(endpoints[0]["ports"], serde_json::json!([443]));
// All-zero array: becomes empty, no fallback port.
assert_eq!(endpoints[1]["ports"], serde_json::json!([]));
assert!(endpoints[1].get("port").is_none());
// Zero scalar port: not promoted, removed.
assert!(endpoints[2].get("ports").is_none());
assert!(endpoints[2].get("port").is_none());
// Positive scalar port: promoted to ports array.
assert_eq!(endpoints[3]["ports"], serde_json::json!([8080]));
assert!(endpoints[3].get("port").is_none());
}

#[test]
fn normalize_endpoint_ports_skips_non_object_endpoints() {
let mut data = serde_json::json!({
"network_policies": {
"p": {
"endpoints": [
"not-an-object",
{"host": "h.test", "port": 443},
42,
]
}
}
});
normalize_endpoint_ports(&mut data);
let endpoints = data["network_policies"]["p"]["endpoints"]
.as_array()
.unwrap();

// Non-object entries are left untouched rather than panicking.
assert_eq!(endpoints[0], serde_json::json!("not-an-object"));
assert_eq!(endpoints[1]["ports"], serde_json::json!([443]));
assert_eq!(endpoints[2], serde_json::json!(42));
}

#[test]
fn normalize_endpoint_ports_empty_array_after_filtering() {
let mut data = serde_json::json!({
"network_policies": {
"p": {
"endpoints": [
{"host": "h.test", "ports": [0], "port": 8080},
]
}
}
});
normalize_endpoint_ports(&mut data);
let endpoints = data["network_policies"]["p"]["endpoints"]
.as_array()
.unwrap();
// Zero-only ports array becomes empty; scalar port is promoted.
assert_eq!(endpoints[0]["ports"], serde_json::json!([8080]));
assert!(endpoints[0].get("port").is_none());
}
}
1 change: 1 addition & 0 deletions crates/openshell-supervisor-network/src/policy_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,7 @@ fn network_endpoint_from_json(
}

let mut ports = endpoint.ports;
ports.retain(|p| *p > 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gator-agent

Warning (CWE-20): This changes the untrusted agent-proposal parser without testing that path. Please add proposal_chunks_from_body cases for mixed [0, 443], zero-only [0] rejection, and [0] with positive scalar fallback, asserting the resulting proto port and ports. The new OPA helper tests cannot protect this separate parser.

if ports.is_empty() && endpoint.port > 0 {
ports.push(endpoint.port);
}
Expand Down
Loading