From c0d4322bcd8b117f776baa7e9dc764d1f889c815 Mon Sep 17 00:00:00 2001 From: Krzysztof Malczuk Date: Fri, 4 Sep 2026 16:33:09 +0100 Subject: [PATCH 1/2] fix(policy): reject unknown endpoint security modes Closes #3046 Validate TLS, enforcement, and access values across policy and provider profile ingress, and prevent runtime parsing from falling back to audit for unknown enforcement values. Signed-off-by: Krzysztof Malczuk --- architecture/security-policy.md | 4 ++ crates/openshell-policy/src/l7_validate.rs | 45 +++++++++++++++++++ crates/openshell-policy/src/lib.rs | 37 ++++++++++++++- crates/openshell-providers/src/profiles.rs | 41 ++++++++++++++++- .../openshell-server/src/grpc/validation.rs | 29 ++++++++++++ .../src/l7/mod.rs | 38 ++++++++++++---- docs/providers/profiles.mdx | 2 + docs/reference/policy-schema.mdx | 6 +-- 8 files changed, 188 insertions(+), 14 deletions(-) diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 62f5837e70..268fb6ad43 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -187,6 +187,10 @@ connection metadata agrees. When request paths overlap, a path endpoint with a higher specificity rank deterministically overrides broader request-processing metadata. Equally specific overlapping endpoints must agree. +Endpoint `tls`, `enforcement`, `access`, and `protocol` strings are validated +before persistence or activation. The supervisor also refuses unknown endpoint +modes defensively; an unrecognized enforcement value never falls back to audit. + Gateway mutation paths validate the complete effective candidate before persistence when the affected sandbox scope is known. Direct replacements, incremental merges and approvals, provider attachment, and profile fanout reject diff --git a/crates/openshell-policy/src/l7_validate.rs b/crates/openshell-policy/src/l7_validate.rs index fe07df123f..60667c75b8 100644 --- a/crates/openshell-policy/src/l7_validate.rs +++ b/crates/openshell-policy/src/l7_validate.rs @@ -107,6 +107,30 @@ mod agent_transport_tests { } } +/// Validate the security-sensitive endpoint fields whose public representation +/// is currently a string. Empty values preserve the documented defaults. +pub fn validate_endpoint_modes(tls: &str, enforcement: &str, access: &str) -> Vec { + let mut errors = Vec::new(); + + if !matches!(tls, "" | "skip" | "terminate" | "passthrough") { + errors.push(format!( + "unknown tls value '{tls}' (expected skip, terminate, or passthrough)" + )); + } + if !matches!(enforcement, "" | "enforce" | "audit") { + errors.push(format!( + "unknown enforcement value '{enforcement}' (expected enforce or audit)" + )); + } + if !matches!(access, "" | "read-only" | "read-write" | "full") { + errors.push(format!( + "unknown access value '{access}' (expected read-only, read-write, or full)" + )); + } + + errors +} + /// Fields extracted from an endpoint definition needed for L7 semantic /// validation. Both profile lint and the runtime validator construct this /// from their own data representation. @@ -250,6 +274,27 @@ mod tests { assert!(errors.is_empty(), "expected no errors, got: {errors:?}"); } + #[test] + fn endpoint_modes_reject_unknown_values() { + let errors = validate_endpoint_modes("skp", "enforc", "read-wirte"); + + assert_eq!(errors.len(), 3); + assert!(errors[0].contains("unknown tls value 'skp'")); + assert!(errors[1].contains("unknown enforcement value 'enforc'")); + assert!(errors[2].contains("unknown access value 'read-wirte'")); + } + + #[test] + fn endpoint_modes_accept_documented_values_and_defaults() { + for tls in ["", "skip", "terminate", "passthrough"] { + for enforcement in ["", "enforce", "audit"] { + for access in ["", "read-only", "read-write", "full"] { + assert!(validate_endpoint_modes(tls, enforcement, access).is_empty()); + } + } + } + } + #[test] fn rejects_unknown_protocol() { let ep = L7EndpointFields { diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index 5f3ec4e452..5f89a3064a 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -38,7 +38,7 @@ pub use compose::{ is_provider_rule_name, provider_rule_name, strip_provider_rule_names, }; pub use l7_validate::{ - L7EndpointFields, L7Protocol, agent_authored_transport_rejection, + L7EndpointFields, L7Protocol, agent_authored_transport_rejection, validate_endpoint_modes, validate_explicit_tcp_additional_fields, validate_l7_endpoint_semantics, }; pub use merge::{ @@ -1814,6 +1814,11 @@ fn validate_sandbox_policy_with_mcp_presence( .unwrap_or(false), }; let mut l7_errors = validate_l7_endpoint_semantics(&fields); + l7_errors.extend(validate_endpoint_modes( + &ep.tls, + &ep.enforcement, + &ep.access, + )); let mut explicit_tcp_fields = Vec::new(); if !ep.enforcement.is_empty() { explicit_tcp_fields.push("enforcement"); @@ -2407,6 +2412,36 @@ network_policies: assert!(policy.filesystem.is_none()); } + #[test] + fn validation_rejects_unknown_security_sensitive_endpoint_values() { + let policy = parse_sandbox_policy( + r" +version: 1 +network_policies: + github_api: + endpoints: + - host: api.github.com + port: 443 + protocol: rest + tls: skp + enforcement: enforc + access: read-wirte +", + ) + .expect("the string-backed protobuf shape accepts syntactically valid YAML"); + + let violations = validate_sandbox_policy(&policy).expect_err("values must be rejected"); + let message = violations + .iter() + .map(ToString::to_string) + .collect::>() + .join("\n"); + + assert!(message.contains("unknown tls value 'skp'")); + assert!(message.contains("unknown enforcement value 'enforc'")); + assert!(message.contains("unknown access value 'read-wirte'")); + } + #[test] fn process_identity_omission_survives_yaml_round_trip() { let policy = parse_sandbox_policy("version: 1\nprocess:\n run_as_user: \"1234\"\n") diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index cbf4d1ba95..353c91d65a 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -16,7 +16,7 @@ use openshell_core::proto::{ }; use openshell_core::secrets::uses_reserved_revision_namespace; use openshell_policy::{ - L7EndpointFields, L7Protocol, validate_explicit_tcp_additional_fields, + L7EndpointFields, L7Protocol, validate_endpoint_modes, validate_explicit_tcp_additional_fields, validate_l7_endpoint_semantics, }; use serde::ser::SerializeStruct; @@ -2343,6 +2343,16 @@ pub fn validate_profile_set( msg, )); } + for msg in + validate_endpoint_modes(&endpoint.tls, &endpoint.enforcement, &endpoint.access) + { + diagnostics.push(ProfileValidationDiagnostic::error( + source, + profile_id, + format!("endpoints[{index}]"), + msg, + )); + } for msg in validate_explicit_tcp_additional_fields( &endpoint.protocol, &additional_l7_profile_fields(endpoint), @@ -5837,6 +5847,35 @@ credentials: // -- L7 endpoint semantic validation (shared with runtime) ---------------- + #[test] + fn validate_rejects_unknown_security_sensitive_endpoint_values() { + let profile = parse_profile_yaml( + r" +id: invalid-modes +display_name: Invalid modes +endpoints: + - host: api.example.com + port: 443 + protocol: rest + tls: skp + enforcement: enforc + access: read-wirte +", + ) + .expect("string values should parse before semantic validation"); + + let diagnostics = validate_profile_set(&[("profile.yaml".to_string(), profile)]); + let message = diagnostics + .iter() + .map(|diagnostic| diagnostic.message.as_str()) + .collect::>() + .join("\n"); + + assert!(message.contains("unknown tls value 'skp'")); + assert!(message.contains("unknown enforcement value 'enforc'")); + assert!(message.contains("unknown access value 'read-wirte'")); + } + #[test] fn validate_rejects_protocol_without_rules_or_access() { let profile = parse_profile_yaml( diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index 2f22199df1..220460152b 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -2006,6 +2006,35 @@ mod tests { assert!(err.message().contains("TLD wildcard")); } + #[test] + fn validate_policy_safety_reports_unknown_enforcement() { + use openshell_core::proto::{NetworkEndpoint, NetworkPolicyRule}; + + let mut policy = openshell_policy::restrictive_default_policy(); + policy.network_policies.insert( + "github_api".into(), + NetworkPolicyRule { + name: "github-api-readonly".into(), + endpoints: vec![NetworkEndpoint { + host: "api.github.com".into(), + port: 443, + protocol: "rest".into(), + enforcement: "enforc".into(), + access: "read-only".into(), + ..Default::default() + }], + ..Default::default() + }, + ); + + let err = validate_policy_safety(&policy).unwrap_err(); + + assert_eq!(err.code(), Code::InvalidArgument); + assert!(err.message().contains("endpoint 0")); + assert!(err.message().contains("unknown enforcement value 'enforc'")); + assert!(err.message().contains("expected enforce or audit")); + } + #[test] fn validate_policy_safety_rejects_invalid_middleware_before_acceptance() { use openshell_core::proto::{MiddlewareEndpointSelector, NetworkMiddlewareConfig}; diff --git a/crates/openshell-supervisor-network/src/l7/mod.rs b/crates/openshell-supervisor-network/src/l7/mod.rs index 70a980ba2d..012d0d5e55 100644 --- a/crates/openshell-supervisor-network/src/l7/mod.rs +++ b/crates/openshell-supervisor-network/src/l7/mod.rs @@ -23,7 +23,8 @@ pub(crate) mod websocket; pub use openshell_policy::L7Protocol; use openshell_policy::{ - L7EndpointFields, validate_explicit_tcp_additional_fields, validate_l7_endpoint_semantics, + L7EndpointFields, validate_endpoint_modes, validate_explicit_tcp_additional_fields, + validate_l7_endpoint_semantics, }; pub(crate) fn build_credential_endpoint_mismatch_finding( @@ -177,9 +178,16 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { let protocol_val = get_object_str(val, "protocol")?; let protocol = L7Protocol::parse(&protocol_val)?; - let tls = match get_object_str(val, "tls").as_deref() { - Some("skip") => TlsMode::Skip, - Some("terminate") => { + let tls_value = get_object_str(val, "tls").unwrap_or_default(); + let enforcement_value = get_object_str(val, "enforcement").unwrap_or_default(); + let access_value = get_object_str(val, "access").unwrap_or_default(); + if !validate_endpoint_modes(&tls_value, &enforcement_value, &access_value).is_empty() { + return None; + } + + let tls = match tls_value.as_str() { + "skip" => TlsMode::Skip, + "terminate" => { let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(openshell_ocsf::ActivityId::Other) .severity(openshell_ocsf::SeverityId::Medium) @@ -191,7 +199,7 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { openshell_ocsf::ocsf_emit!(event); TlsMode::Auto } - Some("passthrough") => { + "passthrough" => { let event = openshell_ocsf::NetworkActivityBuilder::new(openshell_ocsf::ctx::ctx()) .activity(openshell_ocsf::ActivityId::Other) .severity(openshell_ocsf::SeverityId::Medium) @@ -203,12 +211,14 @@ pub fn parse_l7_config(val: ®orus::Value) -> Option { openshell_ocsf::ocsf_emit!(event); TlsMode::Auto } - _ => TlsMode::Auto, + "" => TlsMode::Auto, + _ => unreachable!("endpoint modes were validated above"), }; - let enforcement = match get_object_str(val, "enforcement").as_deref() { - Some("enforce") => EnforcementMode::Enforce, - _ => EnforcementMode::Audit, + let enforcement = match enforcement_value.as_str() { + "enforce" => EnforcementMode::Enforce, + "" | "audit" => EnforcementMode::Audit, + _ => unreachable!("endpoint modes were validated above"), }; let allow_encoded_slash = get_object_bool(val, "allow_encoded_slash").unwrap_or(false); @@ -1762,6 +1772,16 @@ mod tests { assert_eq!(config.enforcement, EnforcementMode::Audit); } + #[test] + fn parse_l7_config_rejects_unknown_enforcement() { + let val = regorus::Value::from_json_str( + r#"{"protocol": "rest", "enforcement": "enforc", "access": "read-only", "host": "api.example.com", "port": 443}"#, + ) + .unwrap(); + + assert!(parse_l7_config(&val).is_none()); + } + #[test] fn parse_credential_signing_sigv4() { let val = regorus::Value::from_json_str( diff --git a/docs/providers/profiles.mdx b/docs/providers/profiles.mdx index c310c02b15..cc7a8dd7ba 100644 --- a/docs/providers/profiles.mdx +++ b/docs/providers/profiles.mdx @@ -437,6 +437,8 @@ environment value under the actual environment variable key. `endpoints` contains the same endpoint object shape as sandbox network policy. A profile can use access presets, protocol-specific allow rules, deny rules, WebSocket credential rewriting, request body credential rewriting, GraphQL fields, and SSRF IP allowlists. Because profile credentials are not mapped to individual endpoints, OpenShell conservatively treats every endpoint in a profile that declares credentials as credentialed. Such endpoints require L7 inspection and cannot use `tls: skip` unless the profile explicitly sets `allow_uninspected_credentials: true`. +Profile validation rejects unknown `tls`, `enforcement`, and `access` values before the profile can contribute policy to a sandbox. + `binaries` contains the executable paths allowed to reach the profile endpoints when the profile contributes policy to a sandbox. `inference_capable` marks profiles that are intended to participate in inference workflows. It does not currently mount or configure `inference.local`. diff --git a/docs/reference/policy-schema.mdx b/docs/reference/policy-schema.mdx index 29eafa60de..7f8aaf8dc1 100644 --- a/docs/reference/policy-schema.mdx +++ b/docs/reference/policy-schema.mdx @@ -164,9 +164,9 @@ Each endpoint defines a reachable destination and optional inspection rules. | `port` | integer | Yes | TCP port number. | | `path` | string | No | Optional HTTP path glob used to select between L7 endpoints that share the same host and port. Empty means all paths. Use this when REST and GraphQL live under the same host, such as `/repos/**` and `/graphql`. | | `protocol` | string | No | Set to `tcp` with a valid DNS hostname to allow native TCP clients through policy DNS and transparent capture without payload inspection. Omit the field for L4 passthrough through an explicit proxy, including legacy hostless `allowed_ips` endpoints. Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, `graphql` for GraphQL-over-HTTP operation inspection, `mcp` for MCP Streamable HTTP request inspection, or `json-rpc` for generic JSON-RPC-over-HTTP method inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Provider-credentialed endpoints require an inspected protocol unless `allow_uninspected_credentials` is explicitly set. | -| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. Provider-credentialed endpoints reject `tls: skip` unless `allow_uninspected_credentials` is explicitly set. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. | -| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. | -| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. Not valid on `protocol: mcp` or `protocol: json-rpc`; MCP uses explicit rules unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile, and JSON-RPC always uses explicit rules. | +| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. Provider-credentialed endpoints reject `tls: skip` unless `allow_uninspected_credentials` is explicitly set. The values `terminate` and `passthrough` are deprecated and log a warning; they are still accepted for backward compatibility but have no effect on behavior. Other values are rejected before activation. | +| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. Other values are rejected before activation rather than interpreted as audit mode. | +| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`; other values are rejected before activation. Mutually exclusive with `rules`. Not valid on `protocol: mcp` or `protocol: json-rpc`; MCP uses explicit rules unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile, and JSON-RPC always uses explicit rules. | | `rules` | list of allow rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. | | `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. | | `allowed_ips` | list of string | No | CIDR or IP allowlist for SSRF override. Exact user-declared hostname endpoints may resolve to RFC 1918 private addresses without this field, but wildcard, hostless, and policy-advisor-proposed endpoints still require `allowed_ips` for private resolved IPs. A hostless allowlist is valid only for the legacy proxy path and cannot be combined with `protocol: tcp`. Entries overlapping loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), or unspecified (`0.0.0.0`) are rejected at load time. | From c1c3bdd22553aeb8b35b4f0df5d8285c533fbe1c Mon Sep 17 00:00:00 2001 From: Krzysztof Malczuk Date: Mon, 7 Sep 2026 11:57:50 +0100 Subject: [PATCH 2/2] fix(policy)!: use enums for endpoint security modes Replace the public TLS, enforcement, and access strings with protobuf enums and carry the typed values through policy composition, provider profiles, drivers, and runtime conversion. Preserve the documented YAML spellings, reject unknown and invalid numeric enum values consistently, and update generated Go bindings, SDK conversions, tests, and policy documentation. Signed-off-by: Krzysztof Malczuk --- architecture/security-policy.md | 8 +- crates/openshell-cli/src/policy_update.rs | 18 +- crates/openshell-cli/src/run.rs | 9 +- .../sandbox_name_fallback_integration.rs | 14 +- .../src/policy_map/map.rs | 25 +- .../tests/policy_mapper_matrix.rs | 16 +- crates/openshell-policy/src/ambiguity.rs | 60 +-- crates/openshell-policy/src/compose.rs | 6 +- crates/openshell-policy/src/l7_validate.rs | 73 ++++ crates/openshell-policy/src/lib.rs | 116 ++++-- crates/openshell-policy/src/merge.rs | 228 ++++++---- crates/openshell-policy/src/middleware.rs | 22 +- crates/openshell-providers/src/profiles.rs | 46 +- .../src/mechanistic_mapper.rs | 9 +- crates/openshell-server/src/grpc/policy.rs | 147 ++++--- crates/openshell-server/src/grpc/provider.rs | 10 +- .../openshell-server/src/grpc/validation.rs | 7 +- .../openshell-supervisor-network/src/opa.rs | 52 ++- .../src/policy_local.rs | 16 +- crates/openshell-tui/src/app.rs | 5 +- crates/openshell-tui/src/lib.rs | 13 +- crates/openshell-tui/src/ui/sandbox_draft.rs | 22 +- docs/reference/policy-schema.mdx | 9 + proto/sandbox.proto | 46 +- .../v1/internal/converter/network_policy.go | 12 +- .../internal/converter/network_policy_test.go | 18 +- sdk/go/openshell/v1/types/network_policy.go | 46 +- sdk/go/openshell/v1/types_reexport.go | 36 ++ sdk/go/proto/sandboxv1/sandbox.pb.go | 394 +++++++++++++----- 29 files changed, 1045 insertions(+), 438 deletions(-) diff --git a/architecture/security-policy.md b/architecture/security-policy.md index 268fb6ad43..b9f3dbbded 100644 --- a/architecture/security-policy.md +++ b/architecture/security-policy.md @@ -187,9 +187,11 @@ connection metadata agrees. When request paths overlap, a path endpoint with a higher specificity rank deterministically overrides broader request-processing metadata. Equally specific overlapping endpoints must agree. -Endpoint `tls`, `enforcement`, `access`, and `protocol` strings are validated -before persistence or activation. The supervisor also refuses unknown endpoint -modes defensively; an unrecognized enforcement value never falls back to audit. +Endpoint `tls`, `enforcement`, and `access` use protobuf enums and retain their +named YAML spellings. `protocol` remains a string so the supported protocol set +can evolve, but every ingress validates it before persistence or activation. +The supervisor also refuses unknown enum numbers and protocol values +defensively; an unrecognized enforcement value never falls back to audit. Gateway mutation paths validate the complete effective candidate before persistence when the affected sandbox scope is known. Direct replacements, diff --git a/crates/openshell-cli/src/policy_update.rs b/crates/openshell-cli/src/policy_update.rs index d6ba02e40c..3a815de983 100644 --- a/crates/openshell-cli/src/policy_update.rs +++ b/crates/openshell-cli/src/policy_update.rs @@ -348,8 +348,10 @@ fn parse_add_endpoint_spec(spec: &str) -> Result { port, ports: vec![port], protocol: protocol.to_string(), - enforcement: enforcement.to_string(), - access: access.to_string(), + enforcement: openshell_policy::network_enforcement_mode_from_str(enforcement) + .expect("validated enforcement") as i32, + access: openshell_policy::network_access_preset_from_str(access).expect("validated access") + as i32, ..Default::default() }; apply_add_endpoint_options(spec, &mut endpoint, options)?; @@ -548,8 +550,14 @@ mod tests { let endpoint = &rule.endpoints[0]; assert_eq!(endpoint.host, "realtime.example.com"); assert_eq!(endpoint.protocol, "websocket"); - assert_eq!(endpoint.access, "read-write"); - assert_eq!(endpoint.enforcement, "enforce"); + assert_eq!( + endpoint.access, + openshell_core::proto::NetworkAccessPreset::ReadWrite as i32 + ); + assert_eq!( + endpoint.enforcement, + openshell_core::proto::NetworkEnforcementMode::Enforce as i32 + ); } #[test] @@ -569,7 +577,7 @@ mod tests { panic!("expected add-rule preview"); }; assert_eq!(rule.endpoints[0].protocol, "tcp"); - assert!(rule.endpoints[0].access.is_empty()); + assert_eq!(rule.endpoints[0].access, 0); } #[test] diff --git a/crates/openshell-cli/src/run.rs b/crates/openshell-cli/src/run.rs index 78a794aa30..7ec4ba648e 100644 --- a/crates/openshell-cli/src/run.rs +++ b/crates/openshell-cli/src/run.rs @@ -5942,8 +5942,11 @@ fn format_endpoint(endpoint: &openshell_core::proto::NetworkEndpoint) -> String }; tags.push(layer_tag.to_string()); - if !endpoint.access.is_empty() { - tags.push(format!("access={}", endpoint.access)); + if endpoint.access != 0 { + tags.push(format!( + "access={}", + openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown") + )); } for r in &endpoint.rules { @@ -6878,7 +6881,7 @@ mod tests { host: "host.example.test".to_string(), port: 443, protocol: "rest".to_string(), - access: "read-only".to_string(), + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }; assert_eq!( diff --git a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs index 5b62c7c15c..f26b1c9f07 100644 --- a/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs +++ b/crates/openshell-cli/tests/sandbox_name_fallback_integration.rs @@ -189,8 +189,9 @@ impl OpenShell for TestOpenShell { host: "api.user.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce + as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() @@ -204,8 +205,9 @@ impl OpenShell for TestOpenShell { host: "api.provider.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce + as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() @@ -459,8 +461,8 @@ impl OpenShell for TestOpenShell { host: "api.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() diff --git a/crates/openshell-driver-mxc/src/policy_map/map.rs b/crates/openshell-driver-mxc/src/policy_map/map.rs index 7d9025e5a6..838d8b4186 100644 --- a/crates/openshell-driver-mxc/src/policy_map/map.rs +++ b/crates/openshell-driver-mxc/src/policy_map/map.rs @@ -506,27 +506,25 @@ fn report_endpoint_l7_losses(endpoint: &NetworkEndpoint, path: &str, items: &mut ); } - if !endpoint.tls.is_empty() { - let severity = if endpoint.tls == "skip" { - "warning" - } else { - "error" - }; + if endpoint.tls != 0 { + let tls = openshell_policy::network_tls_mode_to_str(endpoint.tls).unwrap_or("unknown"); + let severity = if tls == "skip" { "warning" } else { "error" }; add_loss( items, &format!("{path}.tls"), severity, &format!( "MXC has no OpenShell TLS inspection mode equivalent for '{}'.", - endpoint.tls + tls ), "TLS inspection mode", "MXC network policy is host-level only.", ); } - if !endpoint.enforcement.is_empty() { - if endpoint.enforcement == "audit" { + if endpoint.enforcement != 0 { + if openshell_policy::network_enforcement_mode_to_str(endpoint.enforcement) == Some("audit") + { add_loss( items, &format!("{path}.enforcement"), @@ -547,15 +545,14 @@ fn report_endpoint_l7_losses(endpoint: &NetworkEndpoint, path: &str, items: &mut } } - if !endpoint.access.is_empty() { + if endpoint.access != 0 { + let access = + openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown"); add_loss( items, &format!("{path}.access"), "error", - &format!( - "MXC has no access preset equivalent for '{}'.", - endpoint.access - ), + &format!("MXC has no access preset equivalent for '{}'.", access), "REST/WebSocket/GraphQL access preset", "MXC cannot enforce method or operation-level access.", ); diff --git a/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs b/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs index 39a94bee9b..335d2faaca 100644 --- a/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs +++ b/crates/openshell-driver-mxc/tests/policy_mapper_matrix.rs @@ -444,7 +444,7 @@ fn b_tls_skip_warning() { "r", NetworkEndpoint { host: "api.example.com".into(), - tls: "skip".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }, ); @@ -459,7 +459,7 @@ fn b_tls_non_skip_error() { "r", NetworkEndpoint { host: "api.example.com".into(), - tls: "terminate".into(), + tls: openshell_core::proto::NetworkTlsMode::Terminate as i32, ..Default::default() }, ); @@ -474,7 +474,7 @@ fn b_enforcement_audit_error() { "r", NetworkEndpoint { host: "api.example.com".into(), - enforcement: "audit".into(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Audit as i32, ..Default::default() }, ); @@ -494,7 +494,7 @@ fn b_enforcement_non_audit_warning() { "r", NetworkEndpoint { host: "api.example.com".into(), - enforcement: "enforce".into(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, ..Default::default() }, ); @@ -514,7 +514,7 @@ fn b_access_error() { "r", NetworkEndpoint { host: "api.example.com".into(), - access: "read-only".into(), + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }, ); @@ -1200,9 +1200,9 @@ fn handled_fields_inventory() { // Two ports → serializes as `ports: [80, 443]` (array form). ports: vec![80, 443], protocol: "graphql".into(), - tls: "skip".into(), - enforcement: "enforce".into(), - access: "full".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::Full as i32, allowed_ips: vec!["10.0.0.1".into()], allow_encoded_slash: true, websocket_credential_rewrite: true, diff --git a/crates/openshell-policy/src/ambiguity.rs b/crates/openshell-policy/src/ambiguity.rs index 2c0b1b1944..6dc2279b98 100644 --- a/crates/openshell-policy/src/ambiguity.rs +++ b/crates/openshell-policy/src/ambiguity.rs @@ -3,7 +3,9 @@ //! Validation for endpoint selectors whose policy-derived behavior conflicts. -use openshell_core::proto::{NetworkEndpoint, SandboxPolicy}; +use openshell_core::proto::{ + NetworkEndpoint, NetworkEnforcementMode, NetworkTlsMode, SandboxPolicy, +}; use std::collections::{BTreeSet, HashSet, VecDeque}; use std::fmt; @@ -172,8 +174,8 @@ fn connection_conflicts(left: &NetworkEndpoint, right: &NetworkEndpoint) -> Vec< push_conflict( &mut conflicts, "tls", - &normalized_tls(&left.tls), - &normalized_tls(&right.tls), + &normalized_tls(left.tls), + &normalized_tls(right.tls), ); push_conflict( &mut conflicts, @@ -196,7 +198,7 @@ fn is_explicit_tcp(protocol: &str) -> bool { fn endpoint_contributes_request_pipeline_metadata(endpoint: &NetworkEndpoint) -> bool { (!endpoint.protocol.is_empty() && !endpoint.protocol.eq_ignore_ascii_case("tcp")) || !endpoint.allowed_ips.is_empty() - || !endpoint.tls.is_empty() + || endpoint.tls != NetworkTlsMode::Unspecified as i32 || endpoint.credential_binding.is_some() } @@ -211,8 +213,8 @@ fn request_pipeline_conflicts(left: &NetworkEndpoint, right: &NetworkEndpoint) - push_conflict( &mut conflicts, "enforcement", - &normalized_enforcement(&left.enforcement), - &normalized_enforcement(&right.enforcement), + &normalized_enforcement(left.enforcement), + &normalized_enforcement(right.enforcement), ); push_conflict( &mut conflicts, @@ -343,19 +345,23 @@ fn push_conflict( } } -fn normalized_tls(value: &str) -> &'static str { - if value.eq_ignore_ascii_case("skip") { - "skip" +fn normalized_tls(value: i32) -> i32 { + if value == NetworkTlsMode::Skip as i32 { + NetworkTlsMode::Skip as i32 + } else if NetworkTlsMode::try_from(value).is_ok() { + NetworkTlsMode::Unspecified as i32 } else { - "auto" + value } } -fn normalized_enforcement(value: &str) -> &'static str { - if value.eq_ignore_ascii_case("enforce") { - "enforce" +fn normalized_enforcement(value: i32) -> i32 { + if value == NetworkEnforcementMode::Enforce as i32 { + NetworkEnforcementMode::Enforce as i32 + } else if NetworkEnforcementMode::try_from(value).is_ok() { + NetworkEnforcementMode::Audit as i32 } else { - "audit" + value } } @@ -707,7 +713,9 @@ fn complement_ranges(ranges: &[(u32, u32)]) -> Vec<(u32, u32)> { #[cfg(test)] mod tests { use super::*; - use openshell_core::proto::{L7Allow, L7Rule, NetworkBinary, NetworkPolicyRule}; + use openshell_core::proto::{ + L7Allow, L7Rule, NetworkAccessPreset, NetworkBinary, NetworkPolicyRule, + }; fn endpoint(host: &str, port: u32) -> NetworkEndpoint { NetworkEndpoint { @@ -764,7 +772,7 @@ mod tests { #[test] fn disjoint_ports_do_not_overlap() { let mut left = endpoint("api.example.com", 443); - left.tls = "skip".to_string(); + left.tls = NetworkTlsMode::Skip as i32; let right = endpoint("api.example.com", 8443); assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty()); } @@ -773,10 +781,10 @@ mod tests { fn compatible_request_rules_may_overlap() { let mut left = endpoint("api.example.com", 443); left.protocol = "rest".to_string(); - left.tls = "skip".to_string(); + left.tls = NetworkTlsMode::Skip as i32; let mut right = left.clone(); - left.access = "read-only".to_string(); - right.access = "read-write".to_string(); + left.access = NetworkAccessPreset::ReadOnly as i32; + right.access = NetworkAccessPreset::ReadWrite as i32; assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty()); } @@ -795,7 +803,7 @@ mod tests { let left = endpoint("api.example.com", 443); let mut right = endpoint("api.example.com", 443); right.protocol = "rest".to_string(); - right.enforcement = "enforce".to_string(); + right.enforcement = NetworkEnforcementMode::Enforce as i32; assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty()); } @@ -868,11 +876,11 @@ mod tests { fn more_specific_path_may_override_request_pipeline_metadata() { let mut left = endpoint("api.example.com", 443); left.protocol = "rest".to_string(); - left.enforcement = "enforce".to_string(); + left.enforcement = NetworkEnforcementMode::Enforce as i32; let mut right = endpoint("api.example.com", 443); right.path = "/graphql".to_string(); right.protocol = "graphql".to_string(); - right.enforcement = "enforce".to_string(); + right.enforcement = NetworkEnforcementMode::Enforce as i32; assert!(find_endpoint_ambiguities(&policy_with(left, right)).is_empty()); } @@ -880,7 +888,7 @@ mod tests { #[test] fn exact_wildcard_tls_conflict_is_rejected() { let mut left = endpoint("*.example.com", 443); - left.tls = "skip".to_string(); + left.tls = NetworkTlsMode::Skip as i32; let right = endpoint("api.example.com", 443); let ambiguities = find_endpoint_ambiguities(&policy_with(left, right)); @@ -1040,7 +1048,7 @@ mod tests { #[test] fn different_binary_lists_do_not_hide_endpoint_ambiguity() { let mut left = endpoint("api.example.com", 443); - left.tls = "skip".to_string(); + left.tls = NetworkTlsMode::Skip as i32; let right = endpoint("api.example.com", 443); assert_eq!( @@ -1053,9 +1061,9 @@ mod tests { fn explicit_tcp_and_omitted_protocol_are_ambiguous_for_native_tcp_eligibility() { let mut explicit_tcp = endpoint("api.example.com", 443); explicit_tcp.protocol = "tcp".to_string(); - explicit_tcp.tls = "skip".to_string(); + explicit_tcp.tls = NetworkTlsMode::Skip as i32; let mut omitted = endpoint("api.example.com", 443); - omitted.tls = "skip".to_string(); + omitted.tls = NetworkTlsMode::Skip as i32; let ambiguities = find_endpoint_ambiguities(&policy_with(explicit_tcp, omitted)); assert_eq!(ambiguities.len(), 1); diff --git a/crates/openshell-policy/src/compose.rs b/crates/openshell-policy/src/compose.rs index 7ca8584d9d..905ab37707 100644 --- a/crates/openshell-policy/src/compose.rs +++ b/crates/openshell-policy/src/compose.rs @@ -104,9 +104,9 @@ mod tests { host: host.to_string(), port: 443, protocol: "rest".to_string(), - tls: String::new(), - enforcement: "enforce".to_string(), - access: "read-write".to_string(), + tls: 0, + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32, rules: Vec::new(), allowed_ips: Vec::new(), ports: Vec::new(), diff --git a/crates/openshell-policy/src/l7_validate.rs b/crates/openshell-policy/src/l7_validate.rs index 60667c75b8..112a721978 100644 --- a/crates/openshell-policy/src/l7_validate.rs +++ b/crates/openshell-policy/src/l7_validate.rs @@ -9,6 +9,79 @@ //! L7 endpoint field combinations, preventing drift between lint-time //! and runtime checks. +use openshell_core::proto::{NetworkAccessPreset, NetworkEnforcementMode, NetworkTlsMode}; + +#[allow(deprecated)] +pub fn network_tls_mode_from_str(value: &str) -> Option { + match value { + "" => Some(NetworkTlsMode::Unspecified), + "skip" => Some(NetworkTlsMode::Skip), + "terminate" => Some(NetworkTlsMode::Terminate), + "passthrough" => Some(NetworkTlsMode::Passthrough), + _ => None, + } +} + +#[allow(deprecated)] +pub fn network_tls_mode_to_str(value: i32) -> Option<&'static str> { + match NetworkTlsMode::try_from(value).ok()? { + NetworkTlsMode::Unspecified => Some(""), + NetworkTlsMode::Skip => Some("skip"), + NetworkTlsMode::Terminate => Some("terminate"), + NetworkTlsMode::Passthrough => Some("passthrough"), + } +} + +pub fn network_enforcement_mode_from_str(value: &str) -> Option { + match value { + "" => Some(NetworkEnforcementMode::Unspecified), + "enforce" => Some(NetworkEnforcementMode::Enforce), + "audit" => Some(NetworkEnforcementMode::Audit), + _ => None, + } +} + +pub fn network_enforcement_mode_to_str(value: i32) -> Option<&'static str> { + match NetworkEnforcementMode::try_from(value).ok()? { + NetworkEnforcementMode::Unspecified => Some(""), + NetworkEnforcementMode::Enforce => Some("enforce"), + NetworkEnforcementMode::Audit => Some("audit"), + } +} + +pub fn network_access_preset_from_str(value: &str) -> Option { + match value { + "" => Some(NetworkAccessPreset::Unspecified), + "read-only" => Some(NetworkAccessPreset::ReadOnly), + "read-write" => Some(NetworkAccessPreset::ReadWrite), + "full" => Some(NetworkAccessPreset::Full), + _ => None, + } +} + +pub fn network_access_preset_to_str(value: i32) -> Option<&'static str> { + match NetworkAccessPreset::try_from(value).ok()? { + NetworkAccessPreset::Unspecified => Some(""), + NetworkAccessPreset::ReadOnly => Some("read-only"), + NetworkAccessPreset::ReadWrite => Some("read-write"), + NetworkAccessPreset::Full => Some("full"), + } +} + +pub fn validate_endpoint_mode_values(tls: i32, enforcement: i32, access: i32) -> Vec { + let mut errors = Vec::new(); + if network_tls_mode_to_str(tls).is_none() { + errors.push(format!("unknown tls enum value {tls}")); + } + if network_enforcement_mode_to_str(enforcement).is_none() { + errors.push(format!("unknown enforcement enum value {enforcement}")); + } + if network_access_preset_to_str(access).is_none() { + errors.push(format!("unknown access enum value {access}")); + } + errors +} + /// Known L7 inspection protocols. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum L7Protocol { diff --git a/crates/openshell-policy/src/lib.rs b/crates/openshell-policy/src/lib.rs index 5f89a3064a..2ba6eee8de 100644 --- a/crates/openshell-policy/src/lib.rs +++ b/crates/openshell-policy/src/lib.rs @@ -38,7 +38,10 @@ pub use compose::{ is_provider_rule_name, provider_rule_name, strip_provider_rule_names, }; pub use l7_validate::{ - L7EndpointFields, L7Protocol, agent_authored_transport_rejection, validate_endpoint_modes, + L7EndpointFields, L7Protocol, agent_authored_transport_rejection, + network_access_preset_from_str, network_access_preset_to_str, + network_enforcement_mode_from_str, network_enforcement_mode_to_str, network_tls_mode_from_str, + network_tls_mode_to_str, validate_endpoint_mode_values, validate_endpoint_modes, validate_explicit_tcp_additional_fields, validate_l7_endpoint_semantics, }; pub use merge::{ @@ -805,6 +808,19 @@ fn yaml_mcp_method( } fn to_proto(raw: PolicyFile) -> Result { + for (policy_name, rule) in &raw.network_policies { + for (endpoint_index, endpoint) in rule.endpoints.iter().enumerate() { + let errors = + validate_endpoint_modes(&endpoint.tls, &endpoint.enforcement, &endpoint.access); + if !errors.is_empty() { + return Err(miette::miette!( + "network policy '{policy_name}': endpoint {endpoint_index}: {}", + errors.join("; ") + )); + } + } + } + let network_middlewares = middleware::into_proto(raw.network_middlewares) .into_diagnostic() .wrap_err("failed to convert network middleware config")?; @@ -841,9 +857,15 @@ fn to_proto(raw: PolicyFile) -> Result { port: normalized_ports.first().copied().unwrap_or(0), ports: normalized_ports, protocol: protocol.clone(), - tls: e.tls, - enforcement: e.enforcement, - access: e.access, + tls: network_tls_mode_from_str(&e.tls) + .expect("endpoint modes validated above") + as i32, + enforcement: network_enforcement_mode_from_str(&e.enforcement) + .expect("endpoint modes validated above") + as i32, + access: network_access_preset_from_str(&e.access) + .expect("endpoint modes validated above") + as i32, rules: allow_rules .into_iter() .map(|r| L7Rule { @@ -1009,9 +1031,15 @@ fn from_proto(policy: &SandboxPolicy) -> PolicyFile { port, ports, protocol, - tls: e.tls.clone(), - enforcement: e.enforcement.clone(), - access: e.access.clone(), + tls: network_tls_mode_to_str(e.tls) + .expect("policy enum values validated before serialization") + .to_string(), + enforcement: network_enforcement_mode_to_str(e.enforcement) + .expect("policy enum values validated before serialization") + .to_string(), + access: network_access_preset_to_str(e.access) + .expect("policy enum values validated before serialization") + .to_string(), rules, allowed_ips: e.allowed_ips.clone(), deny_rules, @@ -1171,6 +1199,7 @@ pub fn parse_sandbox_policy(yaml: &str) -> Result { /// canonical YAML field names (e.g. `filesystem_policy`, not `filesystem`) /// and is round-trippable through `parse_sandbox_policy`. pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result { + validate_policy_enum_values(policy)?; let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone()) .map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?; let yaml_repr = from_proto(&canonical); @@ -1184,6 +1213,7 @@ pub fn serialize_sandbox_policy(policy: &SandboxPolicy) -> Result { /// The shape mirrors the YAML schema used by [`serialize_sandbox_policy`], so /// automation can use the same documented field names in either format. pub fn sandbox_policy_to_json_value(policy: &SandboxPolicy) -> Result { + validate_policy_enum_values(policy)?; let canonical = validate_and_canonicalize_mcp_policy_schema(policy.clone()) .map_err(|error| miette::miette!("cannot serialize invalid sandbox policy: {error}"))?; let json_repr = from_proto(&canonical); @@ -1192,6 +1222,22 @@ pub fn sandbox_policy_to_json_value(policy: &SandboxPolicy) -> Result Result<()> { + for (policy_name, rule) in &policy.network_policies { + for (endpoint_index, endpoint) in rule.endpoints.iter().enumerate() { + let errors = + validate_endpoint_mode_values(endpoint.tls, endpoint.enforcement, endpoint.access); + if !errors.is_empty() { + return Err(miette::miette!( + "network policy '{policy_name}': endpoint {endpoint_index}: {}", + errors.join("; ") + )); + } + } + } + Ok(()) +} + /// Serialize a proto sandbox policy to a pretty-printed JSON string. pub fn serialize_sandbox_policy_json(policy: &SandboxPolicy) -> Result { let json_repr = sandbox_policy_to_json_value(policy)?; @@ -1706,6 +1752,8 @@ fn validate_sandbox_policy_with_mcp_presence( rule.name.clone() }; for (endpoint_index, ep) in rule.endpoints.iter().enumerate() { + let access = network_access_preset_to_str(ep.access).unwrap_or_default(); + let enforcement = network_enforcement_mode_to_str(ep.enforcement).unwrap_or_default(); let explicit_tcp = l7_validate::is_explicit_tcp_protocol(&ep.protocol); if ep.host.trim().is_empty() && explicit_tcp { violations.push(PolicyViolation::MissingTcpEndpointHost { @@ -1803,7 +1851,7 @@ fn validate_sandbox_policy_with_mcp_presence( }); let fields = L7EndpointFields { protocol: &ep.protocol, - access: &ep.access, + access, has_rules: !ep.rules.is_empty(), has_deny_rules: !ep.deny_rules.is_empty(), rules_would_deny_all, @@ -1814,13 +1862,13 @@ fn validate_sandbox_policy_with_mcp_presence( .unwrap_or(false), }; let mut l7_errors = validate_l7_endpoint_semantics(&fields); - l7_errors.extend(validate_endpoint_modes( - &ep.tls, - &ep.enforcement, - &ep.access, + l7_errors.extend(validate_endpoint_mode_values( + ep.tls, + ep.enforcement, + ep.access, )); let mut explicit_tcp_fields = Vec::new(); - if !ep.enforcement.is_empty() { + if ep.enforcement != 0 { explicit_tcp_fields.push("enforcement"); } if !ep.path.is_empty() { @@ -1865,7 +1913,7 @@ fn validate_sandbox_policy_with_mcp_presence( ep.persisted_queries )); } - if ep.protocol == "sql" && ep.enforcement == "enforce" { + if ep.protocol == "sql" && enforcement == "enforce" { l7_errors.push( "SQL enforcement requires full SQL parsing; use enforcement: audit".to_string(), ); @@ -2414,7 +2462,7 @@ network_policies: #[test] fn validation_rejects_unknown_security_sensitive_endpoint_values() { - let policy = parse_sandbox_policy( + let error = parse_sandbox_policy( r" version: 1 network_policies: @@ -2428,18 +2476,32 @@ network_policies: access: read-wirte ", ) - .expect("the string-backed protobuf shape accepts syntactically valid YAML"); - - let violations = validate_sandbox_policy(&policy).expect_err("values must be rejected"); - let message = violations - .iter() - .map(ToString::to_string) - .collect::>() - .join("\n"); + .expect_err("unknown YAML names must be rejected before protobuf conversion"); + let message = error.to_string(); assert!(message.contains("unknown tls value 'skp'")); assert!(message.contains("unknown enforcement value 'enforc'")); assert!(message.contains("unknown access value 'read-wirte'")); + + let mut policy = restrictive_default_policy(); + policy.network_policies.insert( + "unknown_enum".to_string(), + NetworkPolicyRule { + endpoints: vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + enforcement: 99, + ..Default::default() + }], + ..Default::default() + }, + ); + let violations = validate_sandbox_policy(&policy).expect_err("unknown enum must fail"); + assert!( + violations[0] + .to_string() + .contains("unknown enforcement enum value 99") + ); } #[test] @@ -3360,7 +3422,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "api.example.com".into(), port: 443, - tls: "skip".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], binaries: Vec::new(), @@ -3391,7 +3453,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "api.example.com".into(), port: 443, - tls: "skip".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], binaries: Vec::new(), @@ -3415,7 +3477,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "api.example.com".into(), port: 443, - tls: "skip".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], binaries: Vec::new(), @@ -3443,7 +3505,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "*.example.com".into(), port: 443, - tls: "skip".into(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], binaries: Vec::new(), diff --git a/crates/openshell-policy/src/merge.rs b/crates/openshell-policy/src/merge.rs index e6d72d4899..91ad9b6214 100644 --- a/crates/openshell-policy/src/merge.rs +++ b/crates/openshell-policy/src/merge.rs @@ -4,11 +4,13 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use openshell_core::proto::{ - L7Allow, L7DenyRule, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, SandboxPolicy, + L7Allow, L7DenyRule, L7Rule, NetworkAccessPreset, NetworkBinary, NetworkEndpoint, + NetworkEnforcementMode, NetworkPolicyRule, NetworkTlsMode, SandboxPolicy, }; use crate::{ - PolicyViolation, canonicalize_mcp_options, is_provider_rule_name, restrictive_default_policy, + PolicyViolation, canonicalize_mcp_options, is_provider_rule_name, network_access_preset_to_str, + network_enforcement_mode_to_str, network_tls_mode_to_str, restrictive_default_policy, validate_and_canonicalize_sandbox_policy, }; @@ -672,16 +674,19 @@ fn endpoint_attributes_cover(loaded: &NetworkEndpoint, proposed: &NetworkEndpoin if !proposed.protocol.is_empty() && !protocols_match(&loaded.protocol, &proposed.protocol) { return false; } - if !proposed.tls.is_empty() && effective_tls(&loaded.tls) != effective_tls(&proposed.tls) { + if proposed.tls != NetworkTlsMode::Unspecified as i32 + && effective_tls(loaded.tls) != effective_tls(proposed.tls) + { return false; } - if !proposed.enforcement.is_empty() - && effective_enforcement(&loaded.enforcement) - != effective_enforcement(&proposed.enforcement) + if proposed.enforcement != NetworkEnforcementMode::Unspecified as i32 + && effective_enforcement(loaded.enforcement) != effective_enforcement(proposed.enforcement) { return false; } - if !proposed.access.is_empty() && loaded.access != proposed.access { + if proposed.access != NetworkAccessPreset::Unspecified as i32 + && loaded.access != proposed.access + { return false; } @@ -776,15 +781,25 @@ fn protocols_match(left: &str, right: &str) -> bool { } } -fn effective_tls(value: &str) -> &str { +#[allow(deprecated)] +fn effective_tls(value: i32) -> i32 { match value { - "" | "terminate" | "passthrough" => "auto", + value + if value == NetworkTlsMode::Terminate as i32 + || value == NetworkTlsMode::Passthrough as i32 => + { + NetworkTlsMode::Unspecified as i32 + } value => value, } } -fn effective_enforcement(value: &str) -> &str { - if value.is_empty() { "audit" } else { value } +fn effective_enforcement(value: i32) -> i32 { + if value == NetworkEnforcementMode::Unspecified as i32 { + NetworkEnforcementMode::Audit as i32 + } else { + value + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1082,7 +1097,9 @@ fn apply_operation( } })?; ensure_method_path_endpoint(endpoint, host, *port)?; - if endpoint.access.is_empty() && endpoint.rules.is_empty() { + if endpoint.access == NetworkAccessPreset::Unspecified as i32 + && endpoint.rules.is_empty() + { return Err(PolicyMergeError::EndpointHasNoAllowBase { host: host.clone(), port: *port, @@ -1437,27 +1454,27 @@ fn merge_endpoint( existing.mcp.clone_from(&incoming.mcp); existing.json_rpc_max_body_bytes = incoming.json_rpc_max_body_bytes; } - let existing_enforcement = existing.enforcement.clone(); - merge_string_field( + let existing_enforcement = existing.enforcement; + merge_enum_field( &mut existing.enforcement, - &incoming.enforcement, + incoming.enforcement, PolicyMergeWarning::ExistingEnforcementRetained { host: host.clone(), port, - existing: existing_enforcement, - incoming: incoming.enforcement.clone(), + existing: enforcement_label(existing_enforcement), + incoming: enforcement_label(incoming.enforcement), }, warnings, ); - let existing_tls = existing.tls.clone(); - merge_string_field( + let existing_tls = existing.tls; + merge_enum_field( &mut existing.tls, - &incoming.tls, + incoming.tls, PolicyMergeWarning::ExistingTlsRetained { host: host.clone(), port, - existing: existing_tls, - incoming: incoming.tls.clone(), + existing: tls_label(existing_tls), + incoming: tls_label(incoming.tls), }, warnings, ); @@ -1465,28 +1482,28 @@ fn merge_endpoint( if !incoming.rules.is_empty() { expand_existing_access(existing, &host, port, warnings)?; append_unique_l7_rules(&mut existing.rules, &incoming.rules); - if !incoming.access.is_empty() { + if incoming.access != NetworkAccessPreset::Unspecified as i32 { warnings.push(PolicyMergeWarning::IgnoredIncomingAccessBecauseRulesExist { host, port, - incoming: incoming.access.clone(), + incoming: access_label(incoming.access), }); } - } else if !incoming.access.is_empty() { + } else if incoming.access != NetworkAccessPreset::Unspecified as i32 { if !existing.rules.is_empty() { warnings.push(PolicyMergeWarning::IgnoredIncomingAccessBecauseRulesExist { host, port, - incoming: incoming.access.clone(), + incoming: access_label(incoming.access), }); - } else if existing.access.is_empty() { - existing.access.clone_from(&incoming.access); + } else if existing.access == NetworkAccessPreset::Unspecified as i32 { + existing.access = incoming.access; } else if existing.access != incoming.access { warnings.push(PolicyMergeWarning::ExistingAccessRetained { host, port, - existing: existing.access.clone(), - incoming: incoming.access.clone(), + existing: access_label(existing.access), + incoming: access_label(incoming.access), }); } } @@ -1725,17 +1742,17 @@ fn adopt_unset_retained_fields( if adopted.protocol.is_empty() { adopted.protocol.clone_from(&merged.protocol); } - if adopted.tls.is_empty() { - adopted.tls.clone_from(&merged.tls); + if adopted.tls == NetworkTlsMode::Unspecified as i32 { + adopted.tls = merged.tls; } - if adopted.enforcement.is_empty() { - adopted.enforcement.clone_from(&merged.enforcement); + if adopted.enforcement == NetworkEnforcementMode::Unspecified as i32 { + adopted.enforcement = merged.enforcement; } // `merge_endpoint` only touches `access` when the incoming endpoint carries // an access preset or explicit rules, so an endpoint declaring neither keeps // whatever preset is already loaded. - if adopted.access.is_empty() && adopted.rules.is_empty() { - adopted.access.clone_from(&merged.access); + if adopted.access == NetworkAccessPreset::Unspecified as i32 && adopted.rules.is_empty() { + adopted.access = merged.access; } if adopted.persisted_queries.is_empty() { adopted @@ -1827,6 +1844,34 @@ fn merge_string_field( } } +fn merge_enum_field( + existing: &mut i32, + incoming: i32, + warning: PolicyMergeWarning, + warnings: &mut Vec, +) { + if incoming == 0 { + return; + } + if *existing == 0 { + *existing = incoming; + } else if *existing != incoming { + warnings.push(warning); + } +} + +fn tls_label(value: i32) -> String { + network_tls_mode_to_str(value).map_or_else(|| value.to_string(), str::to_owned) +} + +fn enforcement_label(value: i32) -> String { + network_enforcement_mode_to_str(value).map_or_else(|| value.to_string(), str::to_owned) +} + +fn access_label(value: i32) -> String { + network_access_preset_to_str(value).map_or_else(|| value.to_string(), str::to_owned) +} + fn merge_endpoint_ports(existing: &mut NetworkEndpoint, incoming: &NetworkEndpoint) { let mut ports = canonical_ports(existing); for port in canonical_ports(incoming) { @@ -2000,11 +2045,11 @@ fn expand_existing_access( port: u32, warnings: &mut Vec, ) -> Result<(), PolicyMergeError> { - if endpoint.access.is_empty() { + if endpoint.access == NetworkAccessPreset::Unspecified as i32 { return Ok(()); } - let access = endpoint.access.clone(); + let access = access_label(endpoint.access); let expanded = expand_access_preset(&endpoint.protocol, &access).ok_or_else(|| { PolicyMergeError::UnsupportedAccessPreset { host: host.to_string(), @@ -2012,7 +2057,7 @@ fn expand_existing_access( access: access.clone(), } })?; - endpoint.access.clear(); + endpoint.access = NetworkAccessPreset::Unspecified as i32; append_unique_l7_rules(&mut endpoint.rules, &expanded); warnings.push(PolicyMergeWarning::ExpandedAccessPreset { host: host.to_string(), @@ -2223,8 +2268,9 @@ mod tests { use openshell_core::{ mcp::DEFAULT_MCP_PROTOCOL_VERSION, proto::{ - L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, McpOptions, NetworkBinary, - NetworkEndpoint, NetworkPolicyRule, SandboxPolicy, + L7Allow, L7DenyRule, L7QueryMatcher, L7Rule, McpOptions, NetworkAccessPreset, + NetworkBinary, NetworkEndpoint, NetworkEnforcementMode, NetworkPolicyRule, + NetworkTlsMode, SandboxPolicy, }, }; @@ -2279,8 +2325,8 @@ mod tests { fn canonicalize_advisor_expands_existing_inspected_rule_without_l7_downgrade() { let mut existing_endpoint = endpoint("index.crates.io", 443); existing_endpoint.protocol = "rest".to_string(); - existing_endpoint.enforcement = "enforce".to_string(); - existing_endpoint.access = "read-only".to_string(); + existing_endpoint.enforcement = NetworkEnforcementMode::Enforce as i32; + existing_endpoint.access = NetworkAccessPreset::ReadOnly as i32; let existing = NetworkPolicyRule { name: "cargo-registry".to_string(), endpoints: vec![existing_endpoint.clone()], @@ -2323,8 +2369,8 @@ mod tests { let base = SandboxPolicy::default(); let mut provider_endpoint = endpoint("api.example.com", 443); provider_endpoint.protocol = "rest".to_string(); - provider_endpoint.enforcement = "enforce".to_string(); - provider_endpoint.access = "read-only".to_string(); + provider_endpoint.enforcement = NetworkEnforcementMode::Enforce as i32; + provider_endpoint.access = NetworkAccessPreset::ReadOnly as i32; provider_endpoint.provider_credentialed = true; let mut effective = SandboxPolicy::default(); effective.network_policies.insert( @@ -2351,7 +2397,10 @@ mod tests { assert_eq!(rule_name, "advisor_example"); assert_eq!(canonical.endpoints[0].protocol, "rest"); - assert_eq!(canonical.endpoints[0].access, "read-only"); + assert_eq!( + canonical.endpoints[0].access, + NetworkAccessPreset::ReadOnly as i32 + ); assert!(!canonical.endpoints[0].provider_credentialed); assert!(canonical.endpoints[0].advisor_proposed); assert_eq!( @@ -2364,8 +2413,8 @@ mod tests { fn canonicalize_advisor_ignores_endpoint_provenance_when_inferring_contract() { let mut provider_endpoint = endpoint("api.example.com", 443); provider_endpoint.protocol = "rest".to_string(); - provider_endpoint.enforcement = "enforce".to_string(); - provider_endpoint.access = "read-only".to_string(); + provider_endpoint.enforcement = NetworkEnforcementMode::Enforce as i32; + provider_endpoint.access = NetworkAccessPreset::ReadOnly as i32; provider_endpoint.provider_credentialed = true; let mut advisor_endpoint = provider_endpoint.clone(); @@ -2409,7 +2458,10 @@ mod tests { assert_eq!(rule_name, "existing_advisor"); assert_eq!(canonical.endpoints[0].protocol, "rest"); - assert_eq!(canonical.endpoints[0].access, "read-only"); + assert_eq!( + canonical.endpoints[0].access, + NetworkAccessPreset::ReadOnly as i32 + ); } #[test] @@ -2418,8 +2470,8 @@ mod tests { existing_endpoint.port = 80; existing_endpoint.ports = vec![80, 443]; existing_endpoint.protocol = "rest".to_string(); - existing_endpoint.enforcement = "enforce".to_string(); - existing_endpoint.access = "read-only".to_string(); + existing_endpoint.enforcement = NetworkEnforcementMode::Enforce as i32; + existing_endpoint.access = NetworkAccessPreset::ReadOnly as i32; let existing = NetworkPolicyRule { name: "cargo-registry".to_string(), endpoints: vec![existing_endpoint], @@ -2451,7 +2503,10 @@ mod tests { assert_eq!(rule_name, "allow_index_crates_io_443"); assert_eq!(canonical.endpoints[0].ports, vec![443]); assert_eq!(canonical.endpoints[0].protocol, "rest"); - assert_eq!(canonical.endpoints[0].access, "read-only"); + assert_eq!( + canonical.endpoints[0].access, + NetworkAccessPreset::ReadOnly as i32 + ); let merged = merge_policy( base, @@ -3441,8 +3496,8 @@ mod tests { assert!(!policy_covers_rule(&loaded, &different_body)); let mut explicit_defaults = loaded_endpoint; - explicit_defaults.tls = "passthrough".to_string(); - explicit_defaults.enforcement = "audit".to_string(); + explicit_defaults.tls = 3; // deprecated passthrough compatibility value + explicit_defaults.enforcement = NetworkEnforcementMode::Audit as i32; let runtime_defaults = rule_with_authorizations( "proposed", vec![explicit_defaults.clone()], @@ -3450,7 +3505,7 @@ mod tests { ); assert!(policy_covers_rule(&loaded, &runtime_defaults)); - explicit_defaults.tls = "terminate".to_string(); + explicit_defaults.tls = 2; // deprecated terminate compatibility value let legacy_terminate = rule_with_authorizations( "proposed", vec![explicit_defaults.clone()], @@ -3458,7 +3513,7 @@ mod tests { ); assert!(policy_covers_rule(&loaded, &legacy_terminate)); - explicit_defaults.tls = "skip".to_string(); + explicit_defaults.tls = NetworkTlsMode::Skip as i32; let skip_tls = rule_with_authorizations( "proposed", vec![explicit_defaults.clone()], @@ -3466,8 +3521,8 @@ mod tests { ); assert!(!policy_covers_rule(&loaded, &skip_tls)); - explicit_defaults.tls.clear(); - explicit_defaults.enforcement = "enforce".to_string(); + explicit_defaults.tls = 0; + explicit_defaults.enforcement = NetworkEnforcementMode::Enforce as i32; let different_runtime_scalars = rule_with_authorizations("proposed", vec![explicit_defaults], &["/usr/bin/client"]); assert!(!policy_covers_rule(&loaded, &different_runtime_scalars)); @@ -3675,7 +3730,7 @@ mod tests { port: 443, ports: vec![443], protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, rules: vec![rest_rule("GET", "/repos/**")], ..Default::default() }], @@ -3694,7 +3749,7 @@ mod tests { let rule = &result.policy.network_policies["existing"]; let endpoint = &rule.endpoints[0]; assert_eq!(endpoint.protocol, "rest"); - assert_eq!(endpoint.enforcement, "enforce"); + assert_eq!(endpoint.enforcement, NetworkEnforcementMode::Enforce as i32); assert_eq!(endpoint.rules.len(), 1); assert_eq!(rule.binaries.len(), 2); } @@ -3836,7 +3891,7 @@ mod tests { port: 443, ports: vec![443], protocol: "websocket".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..Default::default() }], ..Default::default() @@ -3850,7 +3905,7 @@ mod tests { port: 443, ports: vec![443], protocol: "websocket".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, websocket_credential_rewrite: true, ..Default::default() }], @@ -3882,7 +3937,7 @@ mod tests { port: 443, ports: vec![443], protocol: "rest".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..Default::default() }], ..Default::default() @@ -3896,7 +3951,7 @@ mod tests { port: 443, ports: vec![443], protocol: "rest".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, request_body_credential_rewrite: true, ..Default::default() }], @@ -3970,7 +4025,7 @@ mod tests { port: 443, ports: vec![443], protocol: "rest".to_string(), - access: "read-only".to_string(), + access: NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() @@ -3988,7 +4043,7 @@ mod tests { .expect("merge should succeed"); let endpoint = &result.policy.network_policies["github"].endpoints[0]; - assert!(endpoint.access.is_empty()); + assert_eq!(endpoint.access, 0); assert_eq!(endpoint.rules.len(), 4); assert!(result.warnings.iter().any(|warning| matches!( warning, @@ -4008,7 +4063,7 @@ mod tests { port: 443, ports: vec![443], protocol: "websocket".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..Default::default() }], ..Default::default() @@ -4026,7 +4081,7 @@ mod tests { .expect("merge should succeed"); let endpoint = &result.policy.network_policies["realtime"].endpoints[0]; - assert!(endpoint.access.is_empty()); + assert_eq!(endpoint.access, 0); assert_eq!(endpoint.rules.len(), 3); assert!(endpoint.rules.contains(&rest_rule("GET", "**"))); assert!(endpoint.rules.contains(&rest_rule("WEBSOCKET_TEXT", "**"))); @@ -4054,7 +4109,7 @@ mod tests { port: 443, ports: vec![443], protocol: "websocket".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..Default::default() }], ..Default::default() @@ -4093,7 +4148,7 @@ mod tests { port: 5432, ports: vec![5432], protocol: "sql".to_string(), - access: "full".to_string(), + access: NetworkAccessPreset::Full as i32, ..Default::default() }], ..Default::default() @@ -4517,8 +4572,8 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-write".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, + access: NetworkAccessPreset::ReadWrite as i32, ..Default::default() }], binaries: vec![NetworkBinary { @@ -4548,7 +4603,7 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, rules: vec![rest_rule("PUT", "/repos/owner/repo/contents/file.md")], ..Default::default() }], @@ -4592,7 +4647,8 @@ mod tests { ); assert_eq!(provider_rule_after.binaries[0].path, "/usr/bin/gh"); assert_eq!( - provider_rule_after.endpoints[0].access, "read-write", + provider_rule_after.endpoints[0].access, + NetworkAccessPreset::ReadWrite as i32, "provider rule's `access` shorthand must remain intact" ); assert!( @@ -4685,7 +4741,7 @@ mod tests { ( "enforcement", NetworkEndpoint { - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, ..endpoint("api.example.com", 443) }, ), @@ -4693,14 +4749,14 @@ mod tests { "protocol", NetworkEndpoint { protocol: "rest".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..endpoint("api.example.com", 443) }, ), ( "tls", NetworkEndpoint { - tls: "skip".to_string(), + tls: NetworkTlsMode::Skip as i32, ..endpoint("api.example.com", 443) }, ), @@ -5073,7 +5129,7 @@ mod tests { rule_with_authorizations( "existing", vec![NetworkEndpoint { - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, ..endpoint("api.example.com", 443) }], &["/usr/bin/trusted"], @@ -5138,7 +5194,7 @@ mod tests { ( "enforcement", NetworkEndpoint { - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, ..endpoint("api.example.com", 443) }, ), @@ -5146,14 +5202,14 @@ mod tests { "protocol", NetworkEndpoint { protocol: "rest".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..endpoint("api.example.com", 443) }, ), ( "tls", NetworkEndpoint { - tls: "skip".to_string(), + tls: NetworkTlsMode::Skip as i32, ..endpoint("api.example.com", 443) }, ), @@ -5161,7 +5217,7 @@ mod tests { "access", NetworkEndpoint { protocol: "rest".to_string(), - access: "read-only".to_string(), + access: NetworkAccessPreset::ReadOnly as i32, ..endpoint("api.example.com", 443) }, ), @@ -5221,7 +5277,7 @@ mod tests { "existing", vec![NetworkEndpoint { protocol: "rest".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..endpoint("api.example.com", 443) }], &["/usr/bin/trusted"], @@ -5230,7 +5286,7 @@ mod tests { "existing", vec![NetworkEndpoint { protocol: "websocket".to_string(), - access: "read-write".to_string(), + access: NetworkAccessPreset::ReadWrite as i32, ..endpoint("api.example.com", 443) }], &["/usr/bin/trusted", "/usr/bin/second"], @@ -6020,7 +6076,7 @@ mod tests { vec![NetworkEndpoint { path: "/graphql".to_string(), protocol: "graphql".to_string(), - access: "read-only".to_string(), + access: NetworkAccessPreset::ReadOnly as i32, ..endpoint("api.github.com", 443) }], &["/usr/bin/only"], @@ -6048,7 +6104,7 @@ mod tests { vec![NetworkEndpoint { path: "/**".to_string(), protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, rules: vec![rest_rule("POST", "/**")], ..endpoint("svc.example.com", 443) }], diff --git a/crates/openshell-policy/src/middleware.rs b/crates/openshell-policy/src/middleware.rs index 9e323e6de0..ee1928f39a 100644 --- a/crates/openshell-policy/src/middleware.rs +++ b/crates/openshell-policy/src/middleware.rs @@ -8,7 +8,7 @@ use std::collections::{BTreeMap, HashMap}; use openshell_core::middleware::{MAX_MIDDLEWARE_CONFIGS, MAX_MIDDLEWARE_SELECTOR_PATTERNS}; use openshell_core::proto::{ MiddlewareEndpointSelector, NetworkEndpoint, NetworkMiddlewareConfig, NetworkPolicyRule, - SandboxPolicy, + NetworkTlsMode, SandboxPolicy, }; use openshell_core::proto_struct::{ ProtoStructError, json_object_to_struct, struct_to_json_object, @@ -168,17 +168,21 @@ where endpoints: rule .endpoints .into_iter() - .map(|endpoint| NetworkEndpoint { - host: endpoint.host, - tls: endpoint.tls, - ..Default::default() + .map(|endpoint| { + Ok(NetworkEndpoint { + host: endpoint.host, + tls: crate::network_tls_mode_from_str(&endpoint.tls) + .ok_or_else(|| format!("unknown tls value '{}'", endpoint.tls))? + as i32, + ..Default::default() + }) }) - .collect(), + .collect::, String>>()?, ..Default::default() }; - (key, rule) + Ok((key, rule)) }) - .collect(); + .collect::, String>>()?; let policy = SandboxPolicy { network_middlewares, network_policies, @@ -292,7 +296,7 @@ pub fn validate(policy: &SandboxPolicy) -> Vec { }; for endpoint in &rule.endpoints { let overlaps_tls_skip = requires_inspection - && endpoint.tls == "skip" + && endpoint.tls == NetworkTlsMode::Skip as i32 && compiled_selector.as_ref().is_some_and(|selector| { HostPattern::new(&endpoint.host) .is_ok_and(|endpoint| selector.may_match_pattern(&endpoint)) diff --git a/crates/openshell-providers/src/profiles.rs b/crates/openshell-providers/src/profiles.rs index 353c91d65a..a764bbbe3b 100644 --- a/crates/openshell-providers/src/profiles.rs +++ b/crates/openshell-providers/src/profiles.rs @@ -16,7 +16,9 @@ use openshell_core::proto::{ }; use openshell_core::secrets::uses_reserved_revision_namespace; use openshell_policy::{ - L7EndpointFields, L7Protocol, validate_endpoint_modes, validate_explicit_tcp_additional_fields, + L7EndpointFields, L7Protocol, network_access_preset_from_str, network_access_preset_to_str, + network_enforcement_mode_from_str, network_enforcement_mode_to_str, network_tls_mode_from_str, + network_tls_mode_to_str, validate_endpoint_modes, validate_explicit_tcp_additional_fields, validate_l7_endpoint_semantics, }; use serde::ser::SerializeStruct; @@ -1430,9 +1432,10 @@ fn endpoint_to_proto(endpoint: &EndpointProfile) -> NetworkEndpoint { host: endpoint.host.clone(), port: endpoint.port, protocol: endpoint.protocol.clone(), - tls: endpoint.tls.clone(), - enforcement: endpoint.enforcement.clone(), - access: endpoint.access.clone(), + tls: network_tls_mode_from_str(&endpoint.tls).map_or(-1, |value| value as i32), + enforcement: network_enforcement_mode_from_str(&endpoint.enforcement) + .map_or(-1, |value| value as i32), + access: network_access_preset_from_str(&endpoint.access).map_or(-1, |value| value as i32), rules: endpoint .rules .as_deref() @@ -1479,9 +1482,14 @@ fn endpoint_from_proto(endpoint: &NetworkEndpoint) -> EndpointProfile { host: endpoint.host.clone(), port: endpoint.port, protocol: endpoint.protocol.clone(), - tls: endpoint.tls.clone(), - access: endpoint.access.clone(), - enforcement: endpoint.enforcement.clone(), + tls: network_tls_mode_to_str(endpoint.tls) + .map_or_else(|| format!("unknown({})", endpoint.tls), str::to_owned), + access: network_access_preset_to_str(endpoint.access) + .map_or_else(|| format!("unknown({})", endpoint.access), str::to_owned), + enforcement: network_enforcement_mode_to_str(endpoint.enforcement).map_or_else( + || format!("unknown({})", endpoint.enforcement), + str::to_owned, + ), rules: if endpoint.rules.is_empty() { None } else { @@ -3308,7 +3316,8 @@ mod tests { endpoint.host == "api.github.com" && endpoint.protocol == "graphql" && endpoint.path == "/graphql" - && endpoint.access == "read-only" + && endpoint.access + == openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 }), "github profile should include read-only GraphQL endpoint" ); @@ -3318,9 +3327,9 @@ mod tests { // transport endpoint (github.com) carries explicit rules // instead so it can allow clone/fetch while blocking push. if endpoint.host == "github.com" { - endpoint.access.is_empty() + endpoint.access == 0 } else { - endpoint.access == "read-only" + endpoint.access == openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 } }), "github API endpoints should be read-only; git transport uses explicit rules" @@ -3342,7 +3351,7 @@ mod tests { // The git transport carries explicit rules rather than an access preset // (an empty preset would otherwise expand to GET/HEAD/OPTIONS). assert!( - git_transport.access.is_empty(), + git_transport.access == 0, "git transport must use explicit rules, not an access preset" ); @@ -5096,7 +5105,10 @@ binaries: let proto = profile.to_proto(); let graphql_ep = &proto.endpoints[0]; - assert_eq!(graphql_ep.access, "read-only"); + assert_eq!( + graphql_ep.access, + openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 + ); assert_eq!(graphql_ep.persisted_queries, "allow_registered"); assert_eq!(graphql_ep.graphql_max_body_bytes, 131_072); assert_eq!(graphql_ep.path, "/graphql"); @@ -5111,7 +5123,10 @@ binaries: let rest_ep = &proto.endpoints[1]; assert_eq!(rest_ep.port, 0); assert_eq!(rest_ep.ports, vec![443, 8443]); - assert_eq!(rest_ep.tls, "terminate"); + assert_eq!( + rest_ep.tls, + openshell_core::proto::NetworkTlsMode::Terminate as i32 + ); assert_eq!(rest_ep.allowed_ips, vec!["10.0.0.0/24"]); assert!(rest_ep.allow_encoded_slash); assert!(rest_ep.allow_uninspected_credentials); @@ -5130,7 +5145,10 @@ binaries: let reparsed = parse_profile_yaml(&profile_to_yaml(&profile).expect("serialize YAML")) .expect("serialized profile should parse"); let reprotoo = reparsed.to_proto(); - assert_eq!(reprotoo.endpoints[0].access, "read-only"); + assert_eq!( + reprotoo.endpoints[0].access, + openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 + ); assert_eq!(reprotoo.endpoints[1].rules.len(), 1); assert_eq!(reprotoo.endpoints[1].deny_rules.len(), 1); assert_eq!(reprotoo.endpoints[1].ports, vec![443, 8443]); diff --git a/crates/openshell-sandbox/src/mechanistic_mapper.rs b/crates/openshell-sandbox/src/mechanistic_mapper.rs index 9be5f8e438..c0920f79a6 100644 --- a/crates/openshell-sandbox/src/mechanistic_mapper.rs +++ b/crates/openshell-sandbox/src/mechanistic_mapper.rs @@ -14,7 +14,8 @@ use openshell_core::net::{is_always_blocked_ip, is_internal_ip, is_known_metadata_hostname}; use openshell_core::proto::{ - DenialSummary, L7Allow, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, PolicyChunk, + DenialSummary, L7Allow, L7Rule, NetworkBinary, NetworkEndpoint, NetworkEnforcementMode, + NetworkPolicyRule, PolicyChunk, }; use std::collections::HashMap; use std::net::IpAddr; @@ -127,7 +128,7 @@ pub fn generate_proposals(summaries: &[DenialSummary]) -> Vec { port: *port, ports: vec![*port], protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: NetworkEnforcementMode::Enforce as i32, rules: l7_rules, advisor_proposed: true, ..Default::default() @@ -594,8 +595,8 @@ mod tests { // L7 fields should be set. assert_eq!(ep.protocol, "rest"); // tls field is no longer set (auto-detection handles it). - assert!(ep.tls.is_empty()); - assert_eq!(ep.enforcement, "enforce"); + assert_eq!(ep.tls, 0); + assert_eq!(ep.enforcement, NetworkEnforcementMode::Enforce as i32); // Should have L7 rules. assert!(!ep.rules.is_empty()); diff --git a/crates/openshell-server/src/grpc/policy.rs b/crates/openshell-server/src/grpc/policy.rs index 7e588fcac8..c77c157dbe 100644 --- a/crates/openshell-server/src/grpc/policy.rs +++ b/crates/openshell-server/src/grpc/policy.rs @@ -333,14 +333,24 @@ fn summarize_endpoint(endpoint: &NetworkEndpoint) -> String { if !endpoint.protocol.is_empty() { parts.push(format!("protocol={}", endpoint.protocol)); } - if !endpoint.access.is_empty() { - parts.push(format!("access={}", endpoint.access)); + if endpoint.access != 0 { + parts.push(format!( + "access={}", + openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown") + )); } - if !endpoint.enforcement.is_empty() { - parts.push(format!("enforcement={}", endpoint.enforcement)); + if endpoint.enforcement != 0 { + parts.push(format!( + "enforcement={}", + openshell_policy::network_enforcement_mode_to_str(endpoint.enforcement) + .unwrap_or("unknown") + )); } - if !endpoint.tls.is_empty() { - parts.push(format!("tls={}", endpoint.tls)); + if endpoint.tls != 0 { + parts.push(format!( + "tls={}", + openshell_policy::network_tls_mode_to_str(endpoint.tls).unwrap_or("unknown") + )); } if endpoint.websocket_credential_rewrite { parts.push("websocket_credential_rewrite=true".to_string()); @@ -3004,7 +3014,7 @@ fn find_uninspected_credentialed_endpoint( let mode = if endpoint.protocol.trim().is_empty() { "L4-only" - } else if endpoint.tls.trim().eq_ignore_ascii_case("skip") { + } else if openshell_policy::network_tls_mode_to_str(endpoint.tls) == Some("skip") { "tls: skip" } else { continue; @@ -4384,7 +4394,7 @@ pub(super) async fn handle_submit_policy_analysis( && let Some(reason) = rule_ref.endpoints.iter().find_map(|endpoint| { openshell_policy::agent_authored_transport_rejection( &endpoint.protocol, - &endpoint.tls, + openshell_policy::network_tls_mode_to_str(endpoint.tls).unwrap_or(""), ) }) { @@ -8205,7 +8215,8 @@ mod tests { host: "api.vendor.example".to_string(), port: 443, protocol: protocol.to_string(), - tls: tls.to_string(), + tls: openshell_policy::network_tls_mode_from_str(tls).expect("valid test TLS mode") + as i32, provider_credentialed: true, allow_uninspected_credentials: allow, ..Default::default() @@ -9134,8 +9145,8 @@ mod tests { let mut policy = test_policy_with_rule("aws", host); let endpoint = &mut policy.network_policies.get_mut("aws").unwrap().endpoints[0]; endpoint.protocol = "rest".to_string(); - endpoint.tls = "terminate".to_string(); - endpoint.access = "full".to_string(); + endpoint.tls = 2; + endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32; endpoint.credential_signing = "sigv4".to_string(); endpoint.signing_service = "s3".to_string(); endpoint.credential_binding = @@ -9147,7 +9158,8 @@ mod tests { fn test_ambiguous_policy() -> ProtoSandboxPolicy { let mut left = test_policy_with_rule("left", "api.example.com"); - left.network_policies.get_mut("left").unwrap().endpoints[0].tls = "skip".to_string(); + left.network_policies.get_mut("left").unwrap().endpoints[0].tls = + openshell_core::proto::NetworkTlsMode::Skip as i32; let right = test_policy_with_rule("right", "api.example.com"); left.network_policies.extend(right.network_policies); left @@ -9230,7 +9242,7 @@ mod tests { host: host.to_string(), port: 443, protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], ..Default::default() @@ -9663,7 +9675,8 @@ mod tests { endpoint.host == "api.github.com" && endpoint.protocol == "graphql" && endpoint.path == "/graphql" - && endpoint.access == "read-only" + && endpoint.access + == openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 }), "github provider policy should include read-only GraphQL endpoint" ); @@ -9672,9 +9685,9 @@ mod tests { // API endpoints stay read-only; the github.com git transport // carries explicit rules so clone/fetch works (see #1769). if endpoint.host == "github.com" { - endpoint.access.is_empty() + endpoint.access == 0 } else { - endpoint.access == "read-only" + endpoint.access == openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 } }), "github API endpoints should be read-only; git transport uses explicit rules" @@ -9961,7 +9974,7 @@ mod tests { .network_policies .remove("provider") .unwrap(); - provider_rule.endpoints[0].tls = "skip".to_string(); + provider_rule.endpoints[0].tls = openshell_core::proto::NetworkTlsMode::Skip as i32; let layers = [ProviderPolicyLayer { rule_name: "_provider_test".to_string(), rule: provider_rule, @@ -10159,8 +10172,8 @@ mod tests { .unwrap() .endpoints[0]; tls_endpoint.protocol = "rest".to_string(); - tls_endpoint.access = "full".to_string(); - tls_endpoint.tls = "skip".to_string(); + tls_endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32; + tls_endpoint.tls = openshell_core::proto::NetworkTlsMode::Skip as i32; let tls_error = handle_update_config( &state, with_user(Request::new(UpdateConfigRequest { @@ -10377,8 +10390,8 @@ mod tests { let mut policy = test_sigv4_policy("bucket.s3.amazonaws.com", None); let endpoint = &mut policy.network_policies.get_mut("aws").unwrap().endpoints[0]; - endpoint.access = "read-write".to_string(); - endpoint.enforcement = "enforce".to_string(); + endpoint.access = openshell_core::proto::NetworkAccessPreset::ReadWrite as i32; + endpoint.enforcement = openshell_core::proto::NetworkEnforcementMode::Enforce as i32; handle_update_config( &state, @@ -10497,7 +10510,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.example.com".to_string(), port: 443, - tls: "skip".to_string(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], ..Default::default() @@ -10568,7 +10581,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.example.com".to_string(), port: 443, - tls: "skip".to_string(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], ..Default::default() @@ -10858,7 +10871,7 @@ mod tests { .expect("custom rule") .endpoints[0]; endpoint.protocol = "rest".to_string(); - endpoint.access = "read-only".to_string(); + endpoint.access = openshell_core::proto::NetworkAccessPreset::ReadOnly as i32; state .store .put_message(&test_sandbox( @@ -11085,8 +11098,8 @@ mod tests { .unwrap() .endpoints[0]; bound_endpoint.protocol = "rest".to_string(); - bound_endpoint.access = "full".to_string(); - bound_endpoint.tls = "terminate".to_string(); + bound_endpoint.access = openshell_core::proto::NetworkAccessPreset::Full as i32; + bound_endpoint.tls = 2; openshell_policy::ensure_sandbox_process_identity(&mut policy); state .store @@ -11298,7 +11311,7 @@ mod tests { port: 443, path: "/**".to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], ..Default::default() @@ -11399,7 +11412,7 @@ mod tests { host: "api.exchange.example.test".to_string(), port: 443, protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], ..Default::default() @@ -11597,7 +11610,7 @@ mod tests { port: 443, path: "/**".to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], ..Default::default() @@ -11784,7 +11797,7 @@ mod tests { host: "api.custom.example".to_string(), port: 443, protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], binaries: Vec::new(), @@ -12587,8 +12600,9 @@ mod tests { host: "shared.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce + as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], binaries: vec![NetworkBinary { @@ -12606,8 +12620,9 @@ mod tests { host: "shared.example.com".to_string(), port: 443, protocol: "graphql".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce + as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, advisor_proposed: true, ..Default::default() }], @@ -13674,7 +13689,7 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "PUT".to_string(), @@ -13862,7 +13877,7 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "PUT".to_string(), @@ -14066,8 +14081,8 @@ mod tests { port: 80, ports: vec![80, 443], protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], binaries: vec![NetworkBinary { @@ -14154,7 +14169,10 @@ mod tests { assert!(!chunk.review_token.is_empty()); let canonical = chunk.proposed_rule.as_ref().unwrap(); assert_eq!(canonical.endpoints[0].protocol, "rest"); - assert_eq!(canonical.endpoints[0].access, "read-only"); + assert_eq!( + canonical.endpoints[0].access, + openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 + ); assert!( canonical.endpoints[0].advisor_proposed, "a new advisor overlay must retain proposal provenance" @@ -14171,14 +14189,20 @@ mod tests { assert_eq!(cargo_rule.endpoints.len(), 1); assert_eq!(cargo_rule.endpoints[0].ports, vec![80, 443]); assert_eq!(cargo_rule.endpoints[0].protocol, "rest"); - assert_eq!(cargo_rule.endpoints[0].access, "read-only"); + assert_eq!( + cargo_rule.endpoints[0].access, + openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 + ); assert_eq!(cargo_rule.binaries.len(), 1); assert_eq!(cargo_rule.binaries[0].path, "/usr/bin/cargo"); let curl_rule = &applied.network_policies["allow_index_crates_io_443"]; assert_eq!(curl_rule.endpoints.len(), 1); assert_eq!(curl_rule.endpoints[0].ports, vec![443]); assert_eq!(curl_rule.endpoints[0].protocol, "rest"); - assert_eq!(curl_rule.endpoints[0].access, "read-only"); + assert_eq!( + curl_rule.endpoints[0].access, + openshell_core::proto::NetworkAccessPreset::ReadOnly as i32 + ); assert!( curl_rule.endpoints[0].advisor_proposed, "the persisted advisor overlay must retain proposal provenance" @@ -14226,7 +14250,8 @@ mod tests { host: "api.example.com".to_string(), port: 443, protocol: "graphql".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce + as i32, rules: vec![L7Rule { allow: Some(L7Allow { // Runtime requires an operation type for @@ -14541,8 +14566,8 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "full".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }], binaries: vec![NetworkBinary { @@ -15169,7 +15194,8 @@ mod tests { host: "api.example.com".to_string(), port: 443, protocol: protocol.to_string(), - tls: tls.to_string(), + tls: openshell_policy::network_tls_mode_from_str(tls) + .expect("test TLS mode must be valid") as i32, ..Default::default() }; let chunk = |name: &str, endpoint: NetworkEndpoint| PolicyChunk { @@ -15653,7 +15679,7 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, deny_rules: vec![L7DenyRule { method: "DELETE".to_string(), path: "/repos/*".to_string(), @@ -15714,7 +15740,7 @@ mod tests { // Match the provider-owned endpoint contract so this test // exercises prover composition rather than a deterministic // application failure. - enforcement: "audit".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Audit as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "PUT".to_string(), @@ -15815,7 +15841,10 @@ mod tests { let effective_policy = get_sandbox_policy(&state, sandbox_id).await; let provider_rule = &effective_policy.network_policies["_provider_work_custom"]; - assert_eq!(provider_rule.endpoints[0].access, "full"); + assert_eq!( + provider_rule.endpoints[0].access, + openshell_core::proto::NetworkAccessPreset::Full as i32 + ); assert_eq!(provider_rule.endpoints[0].deny_rules.len(), 1); assert!(!provider_rule.endpoints[0].advisor_proposed); assert!( @@ -15894,7 +15923,7 @@ mod tests { host: "raw.githubusercontent.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "GET".to_string(), @@ -15936,7 +15965,7 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "PUT".to_string(), @@ -16929,8 +16958,8 @@ mod tests { host: "api.github.com".to_string(), port: 443, protocol: "rest".to_string(), - access: "read-only".to_string(), - enforcement: "enforce".to_string(), + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, ..Default::default() }], binaries: vec![NetworkBinary { @@ -16956,8 +16985,8 @@ mod tests { host: "realtime.example.com".to_string(), port: 443, protocol: "websocket".to_string(), - access: "read-write".to_string(), - enforcement: "enforce".to_string(), + access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32, + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, websocket_credential_rewrite: true, ..Default::default() }], @@ -16984,8 +17013,8 @@ mod tests { host: "slack.com".to_string(), port: 443, protocol: "rest".to_string(), - access: "read-write".to_string(), - enforcement: "enforce".to_string(), + access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32, + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, request_body_credential_rewrite: true, ..Default::default() }], @@ -17278,7 +17307,7 @@ mod tests { port: 443, ports: vec![443], protocol: "rest".to_string(), - access: "read-only".to_string(), + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() @@ -17364,7 +17393,7 @@ mod tests { let policy = SandboxPolicy::decode(latest.policy_payload.as_slice()).unwrap(); let endpoint = &policy.network_policies["github"].endpoints[0]; - assert!(endpoint.access.is_empty()); + assert!(endpoint.access == 0); assert_eq!(endpoint.rules.len(), 4); assert_eq!(endpoint.deny_rules.len(), 1); assert_eq!(endpoint.deny_rules[0].path, "/admin"); @@ -17832,7 +17861,7 @@ mod tests { endpoints: vec![NetworkEndpoint { host: "api.example.com".to_string(), port: 443, - tls: "skip".to_string(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], ..Default::default() diff --git a/crates/openshell-server/src/grpc/provider.rs b/crates/openshell-server/src/grpc/provider.rs index 764c0bbfde..396650f86e 100644 --- a/crates/openshell-server/src/grpc/provider.rs +++ b/crates/openshell-server/src/grpc/provider.rs @@ -5125,7 +5125,7 @@ mod tests { port, path: path.to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }]; handle_import_provider_profiles( @@ -5265,7 +5265,7 @@ mod tests { port: 443, path: "/v1/**".to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }]; let response = handle_import_provider_profiles( @@ -5601,7 +5601,7 @@ mod tests { port: 443, path: "/v1/**".to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }]; let response = handle_update_provider_profiles( @@ -6057,7 +6057,7 @@ mod tests { conflicting_profile.endpoints.push(NetworkEndpoint { host: "api.example.com".to_string(), port: 443, - tls: "skip".to_string(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }); let response = handle_update_provider_profiles( @@ -9664,7 +9664,7 @@ mod tests { port: 443, path: "/v1/**".to_string(), protocol: "rest".to_string(), - access: "full".to_string(), + access: openshell_core::proto::NetworkAccessPreset::Full as i32, ..Default::default() }]; handle_import_provider_profiles( diff --git a/crates/openshell-server/src/grpc/validation.rs b/crates/openshell-server/src/grpc/validation.rs index 220460152b..3b00f67932 100644 --- a/crates/openshell-server/src/grpc/validation.rs +++ b/crates/openshell-server/src/grpc/validation.rs @@ -2019,8 +2019,8 @@ mod tests { host: "api.github.com".into(), port: 443, protocol: "rest".into(), - enforcement: "enforc".into(), - access: "read-only".into(), + enforcement: 99, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, ..Default::default() }], ..Default::default() @@ -2031,8 +2031,7 @@ mod tests { assert_eq!(err.code(), Code::InvalidArgument); assert!(err.message().contains("endpoint 0")); - assert!(err.message().contains("unknown enforcement value 'enforc'")); - assert!(err.message().contains("expected enforce or audit")); + assert!(err.message().contains("unknown enforcement enum value 99")); } #[test] diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 3303aa89b5..2206e227af 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -1921,14 +1921,24 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St if !e.protocol.is_empty() { ep["protocol"] = e.protocol.clone().into(); } - if !e.tls.is_empty() { - ep["tls"] = e.tls.clone().into(); + if e.tls != 0 { + ep["tls"] = openshell_policy::network_tls_mode_to_str(e.tls) + .map_or_else(|| format!("unknown({})", e.tls), str::to_owned) + .into(); } - if !e.enforcement.is_empty() { - ep["enforcement"] = e.enforcement.clone().into(); + if e.enforcement != 0 { + ep["enforcement"] = + openshell_policy::network_enforcement_mode_to_str(e.enforcement) + .map_or_else( + || format!("unknown({})", e.enforcement), + str::to_owned, + ) + .into(); } - if !e.access.is_empty() { - ep["access"] = e.access.clone().into(); + if e.access != 0 { + ep["access"] = openshell_policy::network_access_preset_to_str(e.access) + .map_or_else(|| format!("unknown({})", e.access), str::to_owned) + .into(); } if !e.rules.is_empty() { let rules: Vec = e @@ -3341,7 +3351,7 @@ process: host: "host.k3d.internal".to_string(), port: 56123, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "GET".to_string(), @@ -3880,7 +3890,7 @@ network_policies: host: "api.proto.com".to_string(), port: 8080, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "GET".to_string(), @@ -3952,7 +3962,7 @@ network_policies: port: 8000, path: "/rpc".to_string(), protocol: "json-rpc".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, rules: vec![L7Rule { allow: Some(L7Allow { method: "initialize".to_string(), @@ -4025,7 +4035,7 @@ network_policies: port: 8000, path: "/mcp".to_string(), protocol: "mcp".to_string(), - enforcement: "enforce".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, mcp: Some(McpOptions { versions: vec![DEFAULT_MCP_PROTOCOL_VERSION.as_str().to_string()], ..Default::default() @@ -4992,8 +5002,8 @@ network_policies: host: "registry.npmjs.org".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-only".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadOnly as i32, allow_encoded_slash: true, ..Default::default() }], @@ -5050,8 +5060,8 @@ network_policies: host: "gateway.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "full".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::Full as i32, websocket_credential_rewrite: true, ..Default::default() }], @@ -5108,8 +5118,8 @@ network_policies: host: "bedrock-runtime.us-east-2.amazonaws.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-write".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32, credential_signing: "sigv4".to_string(), signing_service: "bedrock".to_string(), ..Default::default() @@ -5168,8 +5178,8 @@ network_policies: host: "custom-vpc-endpoint.example.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "full".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::Full as i32, credential_signing: "sigv4".to_string(), signing_service: "s3".to_string(), signing_region: "us-west-2".to_string(), @@ -5230,8 +5240,8 @@ network_policies: host: "slack.com".to_string(), port: 443, protocol: "rest".to_string(), - enforcement: "enforce".to_string(), - access: "read-write".to_string(), + enforcement: openshell_core::proto::NetworkEnforcementMode::Enforce as i32, + access: openshell_core::proto::NetworkAccessPreset::ReadWrite as i32, request_body_credential_rewrite: true, ..Default::default() }], @@ -5363,7 +5373,7 @@ network_policies: endpoints: vec![NetworkEndpoint { host: "*.example.com".to_string(), port: 443, - tls: "skip".to_string(), + tls: openshell_core::proto::NetworkTlsMode::Skip as i32, ..Default::default() }], binaries: vec![NetworkBinary { diff --git a/crates/openshell-supervisor-network/src/policy_local.rs b/crates/openshell-supervisor-network/src/policy_local.rs index 520a22adba..ab900398e2 100644 --- a/crates/openshell-supervisor-network/src/policy_local.rs +++ b/crates/openshell-supervisor-network/src/policy_local.rs @@ -1198,9 +1198,14 @@ fn network_endpoint_from_json( host: endpoint.host, port, protocol: endpoint.protocol, - tls: endpoint.tls, - enforcement: endpoint.enforcement, - access: endpoint.access, + tls: openshell_policy::network_tls_mode_from_str(&endpoint.tls) + .ok_or_else(|| format!("unknown tls value '{}'", endpoint.tls))? as i32, + enforcement: openshell_policy::network_enforcement_mode_from_str(&endpoint.enforcement) + .ok_or_else(|| format!("unknown enforcement value '{}'", endpoint.enforcement))? + as i32, + access: openshell_policy::network_access_preset_from_str(&endpoint.access) + .ok_or_else(|| format!("unknown access value '{}'", endpoint.access))? + as i32, rules, allowed_ips: endpoint.allowed_ips, ports, @@ -1553,7 +1558,10 @@ mod tests { let chunks = proposal_chunks_from_body(body).unwrap(); let endpoint = &chunks[0].proposed_rule.as_ref().unwrap().endpoints[0]; assert!(endpoint.protocol.is_empty()); - assert!(endpoint.tls.is_empty()); + assert_eq!( + endpoint.tls, + openshell_core::proto::NetworkTlsMode::Unspecified as i32 + ); } #[test] diff --git a/crates/openshell-tui/src/app.rs b/crates/openshell-tui/src/app.rs index cf7464fcd8..fac0346f0f 100644 --- a/crates/openshell-tui/src/app.rs +++ b/crates/openshell-tui/src/app.rs @@ -3344,14 +3344,15 @@ impl App { } else { endpoint.protocol.as_str() }; - let access = if endpoint.access.is_empty() { + let access = if endpoint.access == 0 { if endpoint.rules.is_empty() { "custom" } else { "rules" } } else { - endpoint.access.as_str() + openshell_policy::network_access_preset_to_str(endpoint.access) + .unwrap_or("unknown") }; let path = if endpoint.path.is_empty() { String::new() diff --git a/crates/openshell-tui/src/lib.rs b/crates/openshell-tui/src/lib.rs index 174f9910d0..db15b9a3b8 100644 --- a/crates/openshell-tui/src/lib.rs +++ b/crates/openshell-tui/src/lib.rs @@ -1229,7 +1229,10 @@ fn render_policy_lines( // Rule header — include L7/TLS/allowed_ips annotation if any endpoint has it. let has_l7 = rule.endpoints.iter().any(|e| !e.protocol.is_empty()); - let has_tls_term = rule.endpoints.iter().any(|e| e.tls == "terminate"); + let has_tls_term = rule + .endpoints + .iter() + .any(|e| openshell_policy::network_tls_mode_to_str(e.tls) == Some("terminate")); let has_allowed_ips = rule.endpoints.iter().any(|e| !e.allowed_ips.is_empty()); let mut annotations = Vec::new(); if has_l7 { @@ -1302,10 +1305,14 @@ fn render_policy_lines( } // Access preset (if set instead of explicit rules). - if !ep.access.is_empty() && ep.rules.is_empty() { + if ep.access != 0 && ep.rules.is_empty() { lines.push(Line::from(vec![ Span::styled(" Access: ", t.muted), - Span::styled(ep.access.clone(), t.text), + Span::styled( + openshell_policy::network_access_preset_to_str(ep.access) + .unwrap_or("unknown"), + t.text, + ), ])); } } diff --git a/crates/openshell-tui/src/ui/sandbox_draft.rs b/crates/openshell-tui/src/ui/sandbox_draft.rs index 470463b7d4..da4961e4c8 100644 --- a/crates/openshell-tui/src/ui/sandbox_draft.rs +++ b/crates/openshell-tui/src/ui/sandbox_draft.rs @@ -818,8 +818,11 @@ fn format_endpoint_summary(endpoint: &NetworkEndpoint) -> String { }; let mut tags = vec![endpoint_layer_label(endpoint).to_string()]; - if !endpoint.access.is_empty() { - tags.push(format!("access={}", endpoint.access)); + if endpoint.access != 0 { + tags.push(format!( + "access={}", + openshell_policy::network_access_preset_to_str(endpoint.access).unwrap_or("unknown") + )); } for rule in &endpoint.rules { if let Some(allow) = &rule.allow { @@ -839,11 +842,18 @@ fn format_endpoint_details(endpoint: &NetworkEndpoint) -> Vec { if !endpoint.path.is_empty() { details.push(format!("Path scope: {}", endpoint.path)); } - if !endpoint.tls.is_empty() { - details.push(format!("TLS: {}", endpoint.tls)); + if endpoint.tls != 0 { + details.push(format!( + "TLS: {}", + openshell_policy::network_tls_mode_to_str(endpoint.tls).unwrap_or("unknown") + )); } - if !endpoint.enforcement.is_empty() { - details.push(format!("Enforcement: {}", endpoint.enforcement)); + if endpoint.enforcement != 0 { + details.push(format!( + "Enforcement: {}", + openshell_policy::network_enforcement_mode_to_str(endpoint.enforcement) + .unwrap_or("unknown") + )); } if endpoint.request_body_credential_rewrite { details.push("Request body credential rewrite".to_string()); diff --git a/docs/reference/policy-schema.mdx b/docs/reference/policy-schema.mdx index 7f8aaf8dc1..43ae503ba7 100644 --- a/docs/reference/policy-schema.mdx +++ b/docs/reference/policy-schema.mdx @@ -189,6 +189,15 @@ Each endpoint defines a reachable destination and optional inspection rules. | `mcp.allow_all_known_mcp_methods` | bool | No | Defaults to `false`. When `true`, enables the endpoint MCP method profile: omitted `rules` allow all MCP-family methods and all tools before `deny_rules`, and omitted rule `method` uses that profile. When unset or `false`, explicit MCP method rules are required; rules with `tool` or `params.name` must set `method: tools/call`. | | `json_rpc` | object | No | JSON-RPC endpoint options. For `protocol: json-rpc`, `json_rpc.max_body_bytes` sets the maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | +The YAML representation keeps the names shown above. Protobuf and generated SDK +clients use `NetworkTlsMode`, `NetworkEnforcementMode`, and +`NetworkAccessPreset` enums for these fields. This is a breaking source and wire +contract change for clients generated from the earlier string fields. +Regenerate bindings from the current protobuf definitions and replace string +assignments with the corresponding enum values. Unspecified TLS keeps automatic +handling, unspecified enforcement keeps the audit default, and unspecified +access selects no preset. Unknown enum numbers are rejected before activation. + **Validation constraints:** - `access` and `rules` are mutually exclusive; setting both is rejected. diff --git a/proto/sandbox.proto b/proto/sandbox.proto index c2b61d0b3a..9a309a8e0a 100644 --- a/proto/sandbox.proto +++ b/proto/sandbox.proto @@ -100,6 +100,40 @@ message NetworkCredentialBinding { string provider = 1; } +// TLS handling for a network endpoint. +enum NetworkTlsMode { + // Auto-detect TLS and terminate it when inspection is configured. + NETWORK_TLS_MODE_UNSPECIFIED = 0; + // Disable TLS detection and relay the connection without inspection. + NETWORK_TLS_MODE_SKIP = 1; + // Deprecated compatibility spelling; behaves like automatic handling. + NETWORK_TLS_MODE_TERMINATE = 2 [deprecated = true]; + // Deprecated compatibility spelling; behaves like automatic handling. + NETWORK_TLS_MODE_PASSTHROUGH = 3 [deprecated = true]; +} + +// Enforcement behavior for inspected network traffic. +enum NetworkEnforcementMode { + // Preserve the existing default: log violations and allow traffic. + NETWORK_ENFORCEMENT_MODE_UNSPECIFIED = 0; + // Deny traffic that does not satisfy the endpoint policy. + NETWORK_ENFORCEMENT_MODE_ENFORCE = 1; + // Log policy violations and allow the traffic. + NETWORK_ENFORCEMENT_MODE_AUDIT = 2; +} + +// Shorthand access policy for an inspected network endpoint. +enum NetworkAccessPreset { + // No access preset; explicit rules may define allowed traffic. + NETWORK_ACCESS_PRESET_UNSPECIFIED = 0; + // Allow read-only operations for the selected protocol. + NETWORK_ACCESS_PRESET_READ_ONLY = 1; + // Allow read and write operations for the selected protocol. + NETWORK_ACCESS_PRESET_READ_WRITE = 2; + // Allow every operation supported by the selected protocol. + NETWORK_ACCESS_PRESET_FULL = 3; +} + // A network endpoint (host + port) with optional L7 inspection config. message NetworkEndpoint { // Hostname or host glob pattern. Exact match is case-insensitive. @@ -112,13 +146,13 @@ message NetworkEndpoint { // Endpoint protocol. "tcp" and "" select L4-only handling; "rest", // "websocket", "graphql", "sql", "json-rpc", and "mcp" select L7 inspection. string protocol = 3; - // TLS handling: "terminate" or "passthrough" (default). - string tls = 4; - // Enforcement mode: "enforce" or "audit" (default). - string enforcement = 5; - // Access preset shorthand: "read-only", "read-write", "full". + // TLS handling. Unspecified enables automatic detection and termination. + NetworkTlsMode tls = 4; + // Enforcement mode. Unspecified preserves the audit default. + NetworkEnforcementMode enforcement = 5; + // Access preset shorthand. Unspecified means no preset. // Mutually exclusive with rules. - string access = 6; + NetworkAccessPreset access = 6; // Explicit L7 rules (mutually exclusive with access). repeated L7Rule rules = 7; // Allowed resolved IP addresses or CIDR ranges for this endpoint. diff --git a/sdk/go/openshell/v1/internal/converter/network_policy.go b/sdk/go/openshell/v1/internal/converter/network_policy.go index 2451f6f426..d51323ed67 100644 --- a/sdk/go/openshell/v1/internal/converter/network_policy.go +++ b/sdk/go/openshell/v1/internal/converter/network_policy.go @@ -67,9 +67,9 @@ func policyNetworkEndpointFromProto(ep *sbv1.NetworkEndpoint) types.PolicyNetwor Host: ep.GetHost(), Port: ep.GetPort(), Protocol: ep.GetProtocol(), - TLS: ep.GetTls(), - Enforcement: ep.GetEnforcement(), - Access: ep.GetAccess(), + TLS: types.NetworkTLSMode(ep.GetTls()), + Enforcement: types.NetworkEnforcementMode(ep.GetEnforcement()), + Access: types.NetworkAccessPreset(ep.GetAccess()), AllowEncodedSlash: ep.GetAllowEncodedSlash(), PersistedQueries: ep.GetPersistedQueries(), GraphqlMaxBodyBytes: ep.GetGraphqlMaxBodyBytes(), @@ -129,9 +129,9 @@ func policyNetworkEndpointToProto(ep *types.PolicyNetworkEndpoint) *sbv1.Network Host: ep.Host, Port: ep.Port, Protocol: ep.Protocol, - Tls: ep.TLS, - Enforcement: ep.Enforcement, - Access: ep.Access, + Tls: sbv1.NetworkTlsMode(ep.TLS), + Enforcement: sbv1.NetworkEnforcementMode(ep.Enforcement), + Access: sbv1.NetworkAccessPreset(ep.Access), AllowEncodedSlash: ep.AllowEncodedSlash, PersistedQueries: ep.PersistedQueries, GraphqlMaxBodyBytes: ep.GraphqlMaxBodyBytes, diff --git a/sdk/go/openshell/v1/internal/converter/network_policy_test.go b/sdk/go/openshell/v1/internal/converter/network_policy_test.go index 5f83a82645..6a4d73dc6f 100644 --- a/sdk/go/openshell/v1/internal/converter/network_policy_test.go +++ b/sdk/go/openshell/v1/internal/converter/network_policy_test.go @@ -22,9 +22,9 @@ func TestNetworkPolicyRuleFromProto(t *testing.T) { Host: "api.example.com", Port: 443, Protocol: "rest", - Tls: "strict", - Enforcement: "enforce", - Access: "allow", + Tls: sbv1.NetworkTlsMode_NETWORK_TLS_MODE_SKIP, + Enforcement: sbv1.NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_ENFORCE, + Access: sbv1.NetworkAccessPreset_NETWORK_ACCESS_PRESET_READ_ONLY, Ports: []uint32{80, 443}, AllowedIps: []string{"10.0.0.1", "10.0.0.2"}, AllowEncodedSlash: true, @@ -101,9 +101,9 @@ func TestNetworkPolicyRuleFromProto(t *testing.T) { assert.Equal(t, "api.example.com", ep.Host) assert.Equal(t, uint32(443), ep.Port) assert.Equal(t, "rest", ep.Protocol) - assert.Equal(t, "strict", ep.TLS) - assert.Equal(t, "enforce", ep.Enforcement) - assert.Equal(t, "allow", ep.Access) + assert.Equal(t, v1.NetworkTLSModeSkip, ep.TLS) + assert.Equal(t, v1.NetworkEnforcementModeEnforce, ep.Enforcement) + assert.Equal(t, v1.NetworkAccessPresetReadOnly, ep.Access) assert.Equal(t, []uint32{80, 443}, ep.Ports) assert.Equal(t, []string{"10.0.0.1", "10.0.0.2"}, ep.AllowedIPs) assert.True(t, ep.AllowEncodedSlash) @@ -181,9 +181,9 @@ func TestNetworkPolicyRuleRoundTrip(t *testing.T) { Host: "gql.example.com", Port: 8080, Protocol: "graphql", - TLS: "permissive", - Enforcement: "audit", - Access: "allow", + TLS: v1.NetworkTLSModeSkip, + Enforcement: v1.NetworkEnforcementModeAudit, + Access: v1.NetworkAccessPresetReadOnly, Ports: []uint32{8080, 8443}, AllowedIPs: []string{"192.168.1.0/24"}, AllowEncodedSlash: false, diff --git a/sdk/go/openshell/v1/types/network_policy.go b/sdk/go/openshell/v1/types/network_policy.go index 69e44e1242..03470a7aba 100644 --- a/sdk/go/openshell/v1/types/network_policy.go +++ b/sdk/go/openshell/v1/types/network_policy.go @@ -13,6 +13,46 @@ type NetworkPolicyRule struct { Binaries []PolicyNetworkBinary } +// NetworkTLSMode controls TLS handling for a policy endpoint. +type NetworkTLSMode int32 + +const ( + // NetworkTLSModeUnspecified uses automatic TLS handling. + NetworkTLSModeUnspecified NetworkTLSMode = 0 + // NetworkTLSModeSkip disables TLS inspection. + NetworkTLSModeSkip NetworkTLSMode = 1 + // NetworkTLSModeTerminate is retained for wire compatibility; prefer unspecified. + NetworkTLSModeTerminate NetworkTLSMode = 2 + // NetworkTLSModePassthrough is retained for wire compatibility; prefer unspecified. + NetworkTLSModePassthrough NetworkTLSMode = 3 +) + +// NetworkEnforcementMode controls whether an endpoint audits or enforces L7 rules. +type NetworkEnforcementMode int32 + +const ( + // NetworkEnforcementModeUnspecified uses the documented audit default. + NetworkEnforcementModeUnspecified NetworkEnforcementMode = 0 + // NetworkEnforcementModeEnforce blocks policy violations. + NetworkEnforcementModeEnforce NetworkEnforcementMode = 1 + // NetworkEnforcementModeAudit logs policy violations without blocking them. + NetworkEnforcementModeAudit NetworkEnforcementMode = 2 +) + +// NetworkAccessPreset selects a predefined endpoint access policy. +type NetworkAccessPreset int32 + +const ( + // NetworkAccessPresetUnspecified selects no access preset. + NetworkAccessPresetUnspecified NetworkAccessPreset = 0 + // NetworkAccessPresetReadOnly permits read operations. + NetworkAccessPresetReadOnly NetworkAccessPreset = 1 + // NetworkAccessPresetReadWrite permits read and write operations. + NetworkAccessPresetReadWrite NetworkAccessPreset = 2 + // NetworkAccessPresetFull permits every operation supported by the protocol. + NetworkAccessPresetFull NetworkAccessPreset = 3 +) + // PolicyNetworkEndpoint describes a full network endpoint with its access controls // as used in sandbox network policy rules. This is distinct from [NetworkEndpoint] // which is the simplified profile-level endpoint (Host, Port, Protocol only). @@ -21,9 +61,9 @@ type PolicyNetworkEndpoint struct { Port uint32 Ports []uint32 Protocol string - TLS string - Enforcement string - Access string + TLS NetworkTLSMode + Enforcement NetworkEnforcementMode + Access NetworkAccessPreset Rules []L7Rule AllowedIPs []string DenyRules []L7DenyRule diff --git a/sdk/go/openshell/v1/types_reexport.go b/sdk/go/openshell/v1/types_reexport.go index b92aa80bc8..a49fc7d96e 100644 --- a/sdk/go/openshell/v1/types_reexport.go +++ b/sdk/go/openshell/v1/types_reexport.go @@ -15,6 +15,42 @@ type NetworkPolicyRule = types.NetworkPolicyRule // PolicyNetworkEndpoint describes a full network endpoint in a sandbox network policy rule. type PolicyNetworkEndpoint = types.PolicyNetworkEndpoint +// NetworkTLSMode controls TLS handling for a policy endpoint. +type NetworkTLSMode = types.NetworkTLSMode + +// NetworkEnforcementMode controls endpoint L7 enforcement behavior. +type NetworkEnforcementMode = types.NetworkEnforcementMode + +// NetworkAccessPreset selects a predefined endpoint access policy. +type NetworkAccessPreset = types.NetworkAccessPreset + +const ( + // NetworkTLSModeUnspecified uses automatic TLS handling. + NetworkTLSModeUnspecified = types.NetworkTLSModeUnspecified + // NetworkTLSModeSkip disables TLS inspection. + NetworkTLSModeSkip = types.NetworkTLSModeSkip + // NetworkTLSModeTerminate is retained for wire compatibility. + NetworkTLSModeTerminate = types.NetworkTLSModeTerminate + // NetworkTLSModePassthrough is retained for wire compatibility. + NetworkTLSModePassthrough = types.NetworkTLSModePassthrough + + // NetworkEnforcementModeUnspecified uses the documented audit default. + NetworkEnforcementModeUnspecified = types.NetworkEnforcementModeUnspecified + // NetworkEnforcementModeEnforce blocks policy violations. + NetworkEnforcementModeEnforce = types.NetworkEnforcementModeEnforce + // NetworkEnforcementModeAudit logs policy violations without blocking them. + NetworkEnforcementModeAudit = types.NetworkEnforcementModeAudit + + // NetworkAccessPresetUnspecified selects no access preset. + NetworkAccessPresetUnspecified = types.NetworkAccessPresetUnspecified + // NetworkAccessPresetReadOnly permits read operations. + NetworkAccessPresetReadOnly = types.NetworkAccessPresetReadOnly + // NetworkAccessPresetReadWrite permits read and write operations. + NetworkAccessPresetReadWrite = types.NetworkAccessPresetReadWrite + // NetworkAccessPresetFull permits every operation supported by the protocol. + NetworkAccessPresetFull = types.NetworkAccessPresetFull +) + // PolicyNetworkBinary identifies a binary subject to network policy enforcement. type PolicyNetworkBinary = types.PolicyNetworkBinary diff --git a/sdk/go/proto/sandboxv1/sandbox.pb.go b/sdk/go/proto/sandboxv1/sandbox.pb.go index 989589002b..c2d009269a 100644 --- a/sdk/go/proto/sandboxv1/sandbox.pb.go +++ b/sdk/go/proto/sandboxv1/sandbox.pb.go @@ -25,6 +25,177 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// TLS handling for a network endpoint. +type NetworkTlsMode int32 + +const ( + // Auto-detect TLS and terminate it when inspection is configured. + NetworkTlsMode_NETWORK_TLS_MODE_UNSPECIFIED NetworkTlsMode = 0 + // Disable TLS detection and relay the connection without inspection. + NetworkTlsMode_NETWORK_TLS_MODE_SKIP NetworkTlsMode = 1 + // Deprecated compatibility spelling; behaves like automatic handling. + // + // Deprecated: Marked as deprecated in sandbox.proto. + NetworkTlsMode_NETWORK_TLS_MODE_TERMINATE NetworkTlsMode = 2 + // Deprecated compatibility spelling; behaves like automatic handling. + // + // Deprecated: Marked as deprecated in sandbox.proto. + NetworkTlsMode_NETWORK_TLS_MODE_PASSTHROUGH NetworkTlsMode = 3 +) + +// Enum value maps for NetworkTlsMode. +var ( + NetworkTlsMode_name = map[int32]string{ + 0: "NETWORK_TLS_MODE_UNSPECIFIED", + 1: "NETWORK_TLS_MODE_SKIP", + 2: "NETWORK_TLS_MODE_TERMINATE", + 3: "NETWORK_TLS_MODE_PASSTHROUGH", + } + NetworkTlsMode_value = map[string]int32{ + "NETWORK_TLS_MODE_UNSPECIFIED": 0, + "NETWORK_TLS_MODE_SKIP": 1, + "NETWORK_TLS_MODE_TERMINATE": 2, + "NETWORK_TLS_MODE_PASSTHROUGH": 3, + } +) + +func (x NetworkTlsMode) Enum() *NetworkTlsMode { + p := new(NetworkTlsMode) + *p = x + return p +} + +func (x NetworkTlsMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkTlsMode) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[0].Descriptor() +} + +func (NetworkTlsMode) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[0] +} + +func (x NetworkTlsMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkTlsMode.Descriptor instead. +func (NetworkTlsMode) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{0} +} + +// Enforcement behavior for inspected network traffic. +type NetworkEnforcementMode int32 + +const ( + // Preserve the existing default: log violations and allow traffic. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_UNSPECIFIED NetworkEnforcementMode = 0 + // Deny traffic that does not satisfy the endpoint policy. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_ENFORCE NetworkEnforcementMode = 1 + // Log policy violations and allow the traffic. + NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_AUDIT NetworkEnforcementMode = 2 +) + +// Enum value maps for NetworkEnforcementMode. +var ( + NetworkEnforcementMode_name = map[int32]string{ + 0: "NETWORK_ENFORCEMENT_MODE_UNSPECIFIED", + 1: "NETWORK_ENFORCEMENT_MODE_ENFORCE", + 2: "NETWORK_ENFORCEMENT_MODE_AUDIT", + } + NetworkEnforcementMode_value = map[string]int32{ + "NETWORK_ENFORCEMENT_MODE_UNSPECIFIED": 0, + "NETWORK_ENFORCEMENT_MODE_ENFORCE": 1, + "NETWORK_ENFORCEMENT_MODE_AUDIT": 2, + } +) + +func (x NetworkEnforcementMode) Enum() *NetworkEnforcementMode { + p := new(NetworkEnforcementMode) + *p = x + return p +} + +func (x NetworkEnforcementMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkEnforcementMode) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[1].Descriptor() +} + +func (NetworkEnforcementMode) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[1] +} + +func (x NetworkEnforcementMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkEnforcementMode.Descriptor instead. +func (NetworkEnforcementMode) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{1} +} + +// Shorthand access policy for an inspected network endpoint. +type NetworkAccessPreset int32 + +const ( + // No access preset; explicit rules may define allowed traffic. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_UNSPECIFIED NetworkAccessPreset = 0 + // Allow read-only operations for the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_READ_ONLY NetworkAccessPreset = 1 + // Allow read and write operations for the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_READ_WRITE NetworkAccessPreset = 2 + // Allow every operation supported by the selected protocol. + NetworkAccessPreset_NETWORK_ACCESS_PRESET_FULL NetworkAccessPreset = 3 +) + +// Enum value maps for NetworkAccessPreset. +var ( + NetworkAccessPreset_name = map[int32]string{ + 0: "NETWORK_ACCESS_PRESET_UNSPECIFIED", + 1: "NETWORK_ACCESS_PRESET_READ_ONLY", + 2: "NETWORK_ACCESS_PRESET_READ_WRITE", + 3: "NETWORK_ACCESS_PRESET_FULL", + } + NetworkAccessPreset_value = map[string]int32{ + "NETWORK_ACCESS_PRESET_UNSPECIFIED": 0, + "NETWORK_ACCESS_PRESET_READ_ONLY": 1, + "NETWORK_ACCESS_PRESET_READ_WRITE": 2, + "NETWORK_ACCESS_PRESET_FULL": 3, + } +) + +func (x NetworkAccessPreset) Enum() *NetworkAccessPreset { + p := new(NetworkAccessPreset) + *p = x + return p +} + +func (x NetworkAccessPreset) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NetworkAccessPreset) Descriptor() protoreflect.EnumDescriptor { + return file_sandbox_proto_enumTypes[2].Descriptor() +} + +func (NetworkAccessPreset) Type() protoreflect.EnumType { + return &file_sandbox_proto_enumTypes[2] +} + +func (x NetworkAccessPreset) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NetworkAccessPreset.Descriptor instead. +func (NetworkAccessPreset) EnumDescriptor() ([]byte, []int) { + return file_sandbox_proto_rawDescGZIP(), []int{2} +} + // Scope that currently controls a setting. type SettingScope int32 @@ -59,11 +230,11 @@ func (x SettingScope) String() string { } func (SettingScope) Descriptor() protoreflect.EnumDescriptor { - return file_sandbox_proto_enumTypes[0].Descriptor() + return file_sandbox_proto_enumTypes[3].Descriptor() } func (SettingScope) Type() protoreflect.EnumType { - return &file_sandbox_proto_enumTypes[0] + return &file_sandbox_proto_enumTypes[3] } func (x SettingScope) Number() protoreflect.EnumNumber { @@ -72,7 +243,7 @@ func (x SettingScope) Number() protoreflect.EnumNumber { // Deprecated: Use SettingScope.Descriptor instead. func (SettingScope) EnumDescriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{0} + return file_sandbox_proto_rawDescGZIP(), []int{3} } // Source used for the policy payload in GetSandboxConfigResponse. @@ -109,11 +280,11 @@ func (x PolicySource) String() string { } func (PolicySource) Descriptor() protoreflect.EnumDescriptor { - return file_sandbox_proto_enumTypes[1].Descriptor() + return file_sandbox_proto_enumTypes[4].Descriptor() } func (PolicySource) Type() protoreflect.EnumType { - return &file_sandbox_proto_enumTypes[1] + return &file_sandbox_proto_enumTypes[4] } func (x PolicySource) Number() protoreflect.EnumNumber { @@ -122,7 +293,7 @@ func (x PolicySource) Number() protoreflect.EnumNumber { // Deprecated: Use PolicySource.Descriptor instead. func (PolicySource) EnumDescriptor() ([]byte, []int) { - return file_sandbox_proto_rawDescGZIP(), []int{1} + return file_sandbox_proto_rawDescGZIP(), []int{4} } // Sandbox security policy configuration. @@ -655,13 +826,13 @@ type NetworkEndpoint struct { // Endpoint protocol. "tcp" and "" select L4-only handling; "rest", // "websocket", "graphql", "sql", "json-rpc", and "mcp" select L7 inspection. Protocol string `protobuf:"bytes,3,opt,name=protocol,proto3" json:"protocol,omitempty"` - // TLS handling: "terminate" or "passthrough" (default). - Tls string `protobuf:"bytes,4,opt,name=tls,proto3" json:"tls,omitempty"` - // Enforcement mode: "enforce" or "audit" (default). - Enforcement string `protobuf:"bytes,5,opt,name=enforcement,proto3" json:"enforcement,omitempty"` - // Access preset shorthand: "read-only", "read-write", "full". + // TLS handling. Unspecified enables automatic detection and termination. + Tls NetworkTlsMode `protobuf:"varint,4,opt,name=tls,proto3,enum=openshell.sandbox.v1.NetworkTlsMode" json:"tls,omitempty"` + // Enforcement mode. Unspecified preserves the audit default. + Enforcement NetworkEnforcementMode `protobuf:"varint,5,opt,name=enforcement,proto3,enum=openshell.sandbox.v1.NetworkEnforcementMode" json:"enforcement,omitempty"` + // Access preset shorthand. Unspecified means no preset. // Mutually exclusive with rules. - Access string `protobuf:"bytes,6,opt,name=access,proto3" json:"access,omitempty"` + Access NetworkAccessPreset `protobuf:"varint,6,opt,name=access,proto3,enum=openshell.sandbox.v1.NetworkAccessPreset" json:"access,omitempty"` // Explicit L7 rules (mutually exclusive with access). Rules []*L7Rule `protobuf:"bytes,7,rep,name=rules,proto3" json:"rules,omitempty"` // Allowed resolved IP addresses or CIDR ranges for this endpoint. @@ -793,25 +964,25 @@ func (x *NetworkEndpoint) GetProtocol() string { return "" } -func (x *NetworkEndpoint) GetTls() string { +func (x *NetworkEndpoint) GetTls() NetworkTlsMode { if x != nil { return x.Tls } - return "" + return NetworkTlsMode_NETWORK_TLS_MODE_UNSPECIFIED } -func (x *NetworkEndpoint) GetEnforcement() string { +func (x *NetworkEndpoint) GetEnforcement() NetworkEnforcementMode { if x != nil { return x.Enforcement } - return "" + return NetworkEnforcementMode_NETWORK_ENFORCEMENT_MODE_UNSPECIFIED } -func (x *NetworkEndpoint) GetAccess() string { +func (x *NetworkEndpoint) GetAccess() NetworkAccessPreset { if x != nil { return x.Access } - return "" + return NetworkAccessPreset_NETWORK_ACCESS_PRESET_UNSPECIFIED } func (x *NetworkEndpoint) GetRules() []*L7Rule { @@ -2113,15 +2284,14 @@ const file_sandbox_proto_rawDesc = "" + "\ainclude\x18\x01 \x03(\tR\ainclude\x12\x18\n" + "\aexclude\x18\x02 \x03(\tR\aexclude\"6\n" + "\x18NetworkCredentialBinding\x12\x1a\n" + - "\bprovider\x18\x01 \x01(\tR\bprovider\"\xdc\n" + - "\n" + + "\bprovider\x18\x01 \x01(\tR\bprovider\"\xdb\v\n" + "\x0fNetworkEndpoint\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\x12\x12\n" + "\x04port\x18\x02 \x01(\rR\x04port\x12\x1a\n" + - "\bprotocol\x18\x03 \x01(\tR\bprotocol\x12\x10\n" + - "\x03tls\x18\x04 \x01(\tR\x03tls\x12 \n" + - "\venforcement\x18\x05 \x01(\tR\venforcement\x12\x16\n" + - "\x06access\x18\x06 \x01(\tR\x06access\x122\n" + + "\bprotocol\x18\x03 \x01(\tR\bprotocol\x126\n" + + "\x03tls\x18\x04 \x01(\x0e2$.openshell.sandbox.v1.NetworkTlsModeR\x03tls\x12N\n" + + "\venforcement\x18\x05 \x01(\x0e2,.openshell.sandbox.v1.NetworkEnforcementModeR\venforcement\x12A\n" + + "\x06access\x18\x06 \x01(\x0e2).openshell.sandbox.v1.NetworkAccessPresetR\x06access\x122\n" + "\x05rules\x18\a \x03(\v2\x1c.openshell.sandbox.v1.L7RuleR\x05rules\x12\x1f\n" + "\vallowed_ips\x18\b \x03(\tR\n" + "allowedIps\x12\x14\n" + @@ -2246,7 +2416,21 @@ const file_sandbox_proto_rawDesc = "" + "\atimeout\x18\x04 \x01(\tR\atimeout\x12%\n" + "\x0ftls_ca_cert_pem\x18\x05 \x01(\fR\ftlsCaCertPem\x12\x1a\n" + "\baudience\x18\x06 \x01(\tR\baudience\x128\n" + - "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport*b\n" + + "\x18allow_insecure_transport\x18\a \x01(\bR\x16allowInsecureTransport*\x97\x01\n" + + "\x0eNetworkTlsMode\x12 \n" + + "\x1cNETWORK_TLS_MODE_UNSPECIFIED\x10\x00\x12\x19\n" + + "\x15NETWORK_TLS_MODE_SKIP\x10\x01\x12\"\n" + + "\x1aNETWORK_TLS_MODE_TERMINATE\x10\x02\x1a\x02\b\x01\x12$\n" + + "\x1cNETWORK_TLS_MODE_PASSTHROUGH\x10\x03\x1a\x02\b\x01*\x8c\x01\n" + + "\x16NetworkEnforcementMode\x12(\n" + + "$NETWORK_ENFORCEMENT_MODE_UNSPECIFIED\x10\x00\x12$\n" + + " NETWORK_ENFORCEMENT_MODE_ENFORCE\x10\x01\x12\"\n" + + "\x1eNETWORK_ENFORCEMENT_MODE_AUDIT\x10\x02*\xa7\x01\n" + + "\x13NetworkAccessPreset\x12%\n" + + "!NETWORK_ACCESS_PRESET_UNSPECIFIED\x10\x00\x12#\n" + + "\x1fNETWORK_ACCESS_PRESET_READ_ONLY\x10\x01\x12$\n" + + " NETWORK_ACCESS_PRESET_READ_WRITE\x10\x02\x12\x1e\n" + + "\x1aNETWORK_ACCESS_PRESET_FULL\x10\x03*b\n" + "\fSettingScope\x12\x1d\n" + "\x19SETTING_SCOPE_UNSPECIFIED\x10\x00\x12\x19\n" + "\x15SETTING_SCOPE_SANDBOX\x10\x01\x12\x18\n" + @@ -2268,86 +2452,92 @@ func file_sandbox_proto_rawDescGZIP() []byte { return file_sandbox_proto_rawDescData } -var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_sandbox_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_sandbox_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_sandbox_proto_goTypes = []any{ - (SettingScope)(0), // 0: openshell.sandbox.v1.SettingScope - (PolicySource)(0), // 1: openshell.sandbox.v1.PolicySource - (*SandboxPolicy)(nil), // 2: openshell.sandbox.v1.SandboxPolicy - (*FilesystemPolicy)(nil), // 3: openshell.sandbox.v1.FilesystemPolicy - (*LandlockPolicy)(nil), // 4: openshell.sandbox.v1.LandlockPolicy - (*ProcessPolicy)(nil), // 5: openshell.sandbox.v1.ProcessPolicy - (*NetworkPolicyRule)(nil), // 6: openshell.sandbox.v1.NetworkPolicyRule - (*NetworkMiddlewareConfig)(nil), // 7: openshell.sandbox.v1.NetworkMiddlewareConfig - (*MiddlewareEndpointSelector)(nil), // 8: openshell.sandbox.v1.MiddlewareEndpointSelector - (*NetworkCredentialBinding)(nil), // 9: openshell.sandbox.v1.NetworkCredentialBinding - (*NetworkEndpoint)(nil), // 10: openshell.sandbox.v1.NetworkEndpoint - (*McpOptions)(nil), // 11: openshell.sandbox.v1.McpOptions - (*GraphqlOperation)(nil), // 12: openshell.sandbox.v1.GraphqlOperation - (*L7DenyRule)(nil), // 13: openshell.sandbox.v1.L7DenyRule - (*L7Rule)(nil), // 14: openshell.sandbox.v1.L7Rule - (*L7Allow)(nil), // 15: openshell.sandbox.v1.L7Allow - (*L7QueryMatcher)(nil), // 16: openshell.sandbox.v1.L7QueryMatcher - (*NetworkBinary)(nil), // 17: openshell.sandbox.v1.NetworkBinary - (*GetSandboxConfigRequest)(nil), // 18: openshell.sandbox.v1.GetSandboxConfigRequest - (*GetGatewayConfigRequest)(nil), // 19: openshell.sandbox.v1.GetGatewayConfigRequest - (*GetGatewayConfigResponse)(nil), // 20: openshell.sandbox.v1.GetGatewayConfigResponse - (*SettingValue)(nil), // 21: openshell.sandbox.v1.SettingValue - (*EffectiveSetting)(nil), // 22: openshell.sandbox.v1.EffectiveSetting - (*GetSandboxConfigResponse)(nil), // 23: openshell.sandbox.v1.GetSandboxConfigResponse - (*SupervisorMiddlewareService)(nil), // 24: openshell.sandbox.v1.SupervisorMiddlewareService - nil, // 25: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - nil, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - nil, // 27: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - nil, // 28: openshell.sandbox.v1.L7DenyRule.QueryEntry - nil, // 29: openshell.sandbox.v1.L7DenyRule.ParamsEntry - nil, // 30: openshell.sandbox.v1.L7Allow.QueryEntry - nil, // 31: openshell.sandbox.v1.L7Allow.ParamsEntry - nil, // 32: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - nil, // 33: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - (*structpb.Struct)(nil), // 34: google.protobuf.Struct + (NetworkTlsMode)(0), // 0: openshell.sandbox.v1.NetworkTlsMode + (NetworkEnforcementMode)(0), // 1: openshell.sandbox.v1.NetworkEnforcementMode + (NetworkAccessPreset)(0), // 2: openshell.sandbox.v1.NetworkAccessPreset + (SettingScope)(0), // 3: openshell.sandbox.v1.SettingScope + (PolicySource)(0), // 4: openshell.sandbox.v1.PolicySource + (*SandboxPolicy)(nil), // 5: openshell.sandbox.v1.SandboxPolicy + (*FilesystemPolicy)(nil), // 6: openshell.sandbox.v1.FilesystemPolicy + (*LandlockPolicy)(nil), // 7: openshell.sandbox.v1.LandlockPolicy + (*ProcessPolicy)(nil), // 8: openshell.sandbox.v1.ProcessPolicy + (*NetworkPolicyRule)(nil), // 9: openshell.sandbox.v1.NetworkPolicyRule + (*NetworkMiddlewareConfig)(nil), // 10: openshell.sandbox.v1.NetworkMiddlewareConfig + (*MiddlewareEndpointSelector)(nil), // 11: openshell.sandbox.v1.MiddlewareEndpointSelector + (*NetworkCredentialBinding)(nil), // 12: openshell.sandbox.v1.NetworkCredentialBinding + (*NetworkEndpoint)(nil), // 13: openshell.sandbox.v1.NetworkEndpoint + (*McpOptions)(nil), // 14: openshell.sandbox.v1.McpOptions + (*GraphqlOperation)(nil), // 15: openshell.sandbox.v1.GraphqlOperation + (*L7DenyRule)(nil), // 16: openshell.sandbox.v1.L7DenyRule + (*L7Rule)(nil), // 17: openshell.sandbox.v1.L7Rule + (*L7Allow)(nil), // 18: openshell.sandbox.v1.L7Allow + (*L7QueryMatcher)(nil), // 19: openshell.sandbox.v1.L7QueryMatcher + (*NetworkBinary)(nil), // 20: openshell.sandbox.v1.NetworkBinary + (*GetSandboxConfigRequest)(nil), // 21: openshell.sandbox.v1.GetSandboxConfigRequest + (*GetGatewayConfigRequest)(nil), // 22: openshell.sandbox.v1.GetGatewayConfigRequest + (*GetGatewayConfigResponse)(nil), // 23: openshell.sandbox.v1.GetGatewayConfigResponse + (*SettingValue)(nil), // 24: openshell.sandbox.v1.SettingValue + (*EffectiveSetting)(nil), // 25: openshell.sandbox.v1.EffectiveSetting + (*GetSandboxConfigResponse)(nil), // 26: openshell.sandbox.v1.GetSandboxConfigResponse + (*SupervisorMiddlewareService)(nil), // 27: openshell.sandbox.v1.SupervisorMiddlewareService + nil, // 28: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + nil, // 29: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + nil, // 30: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + nil, // 31: openshell.sandbox.v1.L7DenyRule.QueryEntry + nil, // 32: openshell.sandbox.v1.L7DenyRule.ParamsEntry + nil, // 33: openshell.sandbox.v1.L7Allow.QueryEntry + nil, // 34: openshell.sandbox.v1.L7Allow.ParamsEntry + nil, // 35: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + nil, // 36: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + (*structpb.Struct)(nil), // 37: google.protobuf.Struct } var file_sandbox_proto_depIdxs = []int32{ - 3, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy - 4, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy - 5, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy - 25, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry - 26, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry - 10, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint - 17, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary - 34, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct - 8, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector - 14, // 9: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule - 13, // 10: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule - 27, // 11: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry - 11, // 12: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions - 9, // 13: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding - 28, // 14: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry - 29, // 15: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry - 15, // 16: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow - 30, // 17: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry - 31, // 18: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry - 32, // 19: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry - 21, // 20: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue - 0, // 21: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope - 2, // 22: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy - 33, // 23: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry - 1, // 24: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource - 24, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService - 6, // 26: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule - 7, // 27: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig - 12, // 28: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation - 16, // 29: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 30: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 31: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 16, // 32: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher - 21, // 33: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue - 22, // 34: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting - 35, // [35:35] is the sub-list for method output_type - 35, // [35:35] is the sub-list for method input_type - 35, // [35:35] is the sub-list for extension type_name - 35, // [35:35] is the sub-list for extension extendee - 0, // [0:35] is the sub-list for field type_name + 6, // 0: openshell.sandbox.v1.SandboxPolicy.filesystem:type_name -> openshell.sandbox.v1.FilesystemPolicy + 7, // 1: openshell.sandbox.v1.SandboxPolicy.landlock:type_name -> openshell.sandbox.v1.LandlockPolicy + 8, // 2: openshell.sandbox.v1.SandboxPolicy.process:type_name -> openshell.sandbox.v1.ProcessPolicy + 28, // 3: openshell.sandbox.v1.SandboxPolicy.network_policies:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry + 29, // 4: openshell.sandbox.v1.SandboxPolicy.network_middlewares:type_name -> openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry + 13, // 5: openshell.sandbox.v1.NetworkPolicyRule.endpoints:type_name -> openshell.sandbox.v1.NetworkEndpoint + 20, // 6: openshell.sandbox.v1.NetworkPolicyRule.binaries:type_name -> openshell.sandbox.v1.NetworkBinary + 37, // 7: openshell.sandbox.v1.NetworkMiddlewareConfig.config:type_name -> google.protobuf.Struct + 11, // 8: openshell.sandbox.v1.NetworkMiddlewareConfig.endpoints:type_name -> openshell.sandbox.v1.MiddlewareEndpointSelector + 0, // 9: openshell.sandbox.v1.NetworkEndpoint.tls:type_name -> openshell.sandbox.v1.NetworkTlsMode + 1, // 10: openshell.sandbox.v1.NetworkEndpoint.enforcement:type_name -> openshell.sandbox.v1.NetworkEnforcementMode + 2, // 11: openshell.sandbox.v1.NetworkEndpoint.access:type_name -> openshell.sandbox.v1.NetworkAccessPreset + 17, // 12: openshell.sandbox.v1.NetworkEndpoint.rules:type_name -> openshell.sandbox.v1.L7Rule + 16, // 13: openshell.sandbox.v1.NetworkEndpoint.deny_rules:type_name -> openshell.sandbox.v1.L7DenyRule + 30, // 14: openshell.sandbox.v1.NetworkEndpoint.graphql_persisted_queries:type_name -> openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry + 14, // 15: openshell.sandbox.v1.NetworkEndpoint.mcp:type_name -> openshell.sandbox.v1.McpOptions + 12, // 16: openshell.sandbox.v1.NetworkEndpoint.credential_binding:type_name -> openshell.sandbox.v1.NetworkCredentialBinding + 31, // 17: openshell.sandbox.v1.L7DenyRule.query:type_name -> openshell.sandbox.v1.L7DenyRule.QueryEntry + 32, // 18: openshell.sandbox.v1.L7DenyRule.params:type_name -> openshell.sandbox.v1.L7DenyRule.ParamsEntry + 18, // 19: openshell.sandbox.v1.L7Rule.allow:type_name -> openshell.sandbox.v1.L7Allow + 33, // 20: openshell.sandbox.v1.L7Allow.query:type_name -> openshell.sandbox.v1.L7Allow.QueryEntry + 34, // 21: openshell.sandbox.v1.L7Allow.params:type_name -> openshell.sandbox.v1.L7Allow.ParamsEntry + 35, // 22: openshell.sandbox.v1.GetGatewayConfigResponse.settings:type_name -> openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry + 24, // 23: openshell.sandbox.v1.EffectiveSetting.value:type_name -> openshell.sandbox.v1.SettingValue + 3, // 24: openshell.sandbox.v1.EffectiveSetting.scope:type_name -> openshell.sandbox.v1.SettingScope + 5, // 25: openshell.sandbox.v1.GetSandboxConfigResponse.policy:type_name -> openshell.sandbox.v1.SandboxPolicy + 36, // 26: openshell.sandbox.v1.GetSandboxConfigResponse.settings:type_name -> openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry + 4, // 27: openshell.sandbox.v1.GetSandboxConfigResponse.policy_source:type_name -> openshell.sandbox.v1.PolicySource + 27, // 28: openshell.sandbox.v1.GetSandboxConfigResponse.supervisor_middleware_services:type_name -> openshell.sandbox.v1.SupervisorMiddlewareService + 9, // 29: openshell.sandbox.v1.SandboxPolicy.NetworkPoliciesEntry.value:type_name -> openshell.sandbox.v1.NetworkPolicyRule + 10, // 30: openshell.sandbox.v1.SandboxPolicy.NetworkMiddlewaresEntry.value:type_name -> openshell.sandbox.v1.NetworkMiddlewareConfig + 15, // 31: openshell.sandbox.v1.NetworkEndpoint.GraphqlPersistedQueriesEntry.value:type_name -> openshell.sandbox.v1.GraphqlOperation + 19, // 32: openshell.sandbox.v1.L7DenyRule.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 33: openshell.sandbox.v1.L7DenyRule.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 34: openshell.sandbox.v1.L7Allow.QueryEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 19, // 35: openshell.sandbox.v1.L7Allow.ParamsEntry.value:type_name -> openshell.sandbox.v1.L7QueryMatcher + 24, // 36: openshell.sandbox.v1.GetGatewayConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.SettingValue + 25, // 37: openshell.sandbox.v1.GetSandboxConfigResponse.SettingsEntry.value:type_name -> openshell.sandbox.v1.EffectiveSetting + 38, // [38:38] is the sub-list for method output_type + 38, // [38:38] is the sub-list for method input_type + 38, // [38:38] is the sub-list for extension type_name + 38, // [38:38] is the sub-list for extension extendee + 0, // [0:38] is the sub-list for field type_name } func init() { file_sandbox_proto_init() } @@ -2367,7 +2557,7 @@ func file_sandbox_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_sandbox_proto_rawDesc), len(file_sandbox_proto_rawDesc)), - NumEnums: 2, + NumEnums: 5, NumMessages: 32, NumExtensions: 0, NumServices: 0,