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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions crates/openshell-supervisor-network/src/l7/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ use crate::opa::PolicyGenerationGuard;
use miette::{Result, miette};
use openshell_ocsf::{
ActionId, ActivityId, DetectionFindingBuilder, DispositionId, Endpoint, FindingInfo,
HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, Url as OcsfUrl,
ocsf_emit,
HttpActivityBuilder, HttpRequest, NetworkActivityBuilder, SeverityId, StatusId, ocsf_emit,
};
use std::path::PathBuf;
use tokio::io::{AsyncRead, AsyncWrite};
Expand Down Expand Up @@ -915,10 +914,7 @@ pub(super) fn middleware_events(
} else {
SeverityId::Medium
})
.http_request(HttpRequest::new(
&req.action,
OcsfUrl::new("http", &ctx.host, &req.target, ctx.port),
))
.http_request(HttpRequest::new(&req.action, ctx.ocsf_url(&req.target)))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, "middleware")
.unmapped("transformed", invocation.transformed)
Expand Down
135 changes: 122 additions & 13 deletions crates/openshell-supervisor-network/src/l7/relay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ impl L7EvalContext {
pub(crate) fn websocket_scheme(&self) -> &'static str {
if self.is_plaintext() { "ws" } else { "wss" }
}

/// OCSF URL for `target` on the inspected transport.
pub(crate) fn ocsf_url(&self, target: &str) -> OcsfUrl {
OcsfUrl::new(self.request_scheme(), &self.host, target, self.port)
}
}

fn request_default_port(ctx: &L7EvalContext) -> Option<u16> {
Expand Down Expand Up @@ -1696,10 +1701,7 @@ fn build_l7_request_event(
.action(action_id)
.disposition(disposition_id)
.severity(severity)
.http_request(HttpRequest::new(
action,
OcsfUrl::new("http", &ctx.host, redacted_target, ctx.port),
))
.http_request(HttpRequest::new(action, ctx.ocsf_url(redacted_target)))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, engine_type)
.message(message)
Expand Down Expand Up @@ -2158,7 +2160,7 @@ where
.severity(severity)
.http_request(HttpRequest::new(
&request_info.action,
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
ctx.ocsf_url(&redacted_target),
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, "l7")
Expand Down Expand Up @@ -2582,7 +2584,7 @@ where
.severity(severity)
.http_request(HttpRequest::new(
&request_info.action,
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
ctx.ocsf_url(&redacted_target),
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, jsonrpc_engine_type(config.protocol))
Expand Down Expand Up @@ -2868,7 +2870,7 @@ where
.severity(severity)
.http_request(HttpRequest::new(
&request_info.action,
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
ctx.ocsf_url(&redacted_target),
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, "l7-graphql")
Expand Down Expand Up @@ -3339,7 +3341,7 @@ fn emit_transformed_body_decision(
.severity(severity)
.http_request(HttpRequest::new(
&request_info.action,
OcsfUrl::new("http", &ctx.host, &request_info.target, ctx.port),
ctx.ocsf_url(&request_info.target),
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.firewall_rule(&ctx.policy_name, engine_type)
Expand Down Expand Up @@ -3573,7 +3575,7 @@ where
.severity(SeverityId::Informational)
.http_request(HttpRequest::new(
&req.action,
OcsfUrl::new("http", &ctx.host, &redacted_target, ctx.port),
ctx.ocsf_url(&redacted_target),
Comment thread
TaylorMutch marked this conversation as resolved.
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.message(format!(
Expand Down Expand Up @@ -3606,7 +3608,7 @@ where
req,
client,
ctx,
"http",
ctx.request_scheme(),
openshell_supervisor_middleware::TransformedBodyPolicy::NotPolicyRelevant,
)
.await?;
Expand Down Expand Up @@ -3680,7 +3682,7 @@ where
let resolver = ctx.secret_resolver.as_deref();
let response_middleware = response_selection
.as_ref()
.map(|exchange| exchange.response_relay(&req_with_auth, ctx, "http"));
.map(|exchange| exchange.response_relay(&req_with_auth, ctx, ctx.request_scheme()));

// Forward request with credential rewriting and relay the response.
// relay_http_request_with_resolver handles both directions: it sends
Expand Down Expand Up @@ -7814,7 +7816,7 @@ network_policies:
assert_eq!(denied_json["unmapped"]["failed"], false);
assert_eq!(
denied_http.format_shorthand(),
"HTTP:POST [MED] DENIED POST http://api.example.test:443/v1/messages \
"HTTP:POST [MED] DENIED POST https://api.example.test/v1/messages \
[policy:rest_api engine:middleware] \
[failed:false transformed:false \
reason:middleware_denied:content-guard:content_match]"
Expand Down Expand Up @@ -7930,6 +7932,88 @@ network_policies:
.unwrap();
}

#[tokio::test]
async fn passthrough_middleware_scheme_follows_transport() {
use openshell_supervisor_middleware::{ChainRunner, MiddlewareRegistry};

let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
for (port, transport, scheme) in [(8443, 443, "https"), (443, 80, "http")] {
seen.lock().unwrap().clear();
let data = format!(
r#"
network_middlewares:
scheme:
middleware: test/scheme
on_error: fail_closed
endpoints:
include: ["api.example.test"]
network_policies:
passthrough_api:
name: passthrough_api
endpoints:
- host: api.example.test
port: {port}
binaries:
- {{ path: /usr/bin/curl }}
"#
);
let engine = Arc::new(OpaEngine::from_strings(TEST_POLICY, &data).unwrap());
let registry = MiddlewareRegistry::connect_services(
vec![Arc::new(SchemeRecorder(seen.clone()))],
Vec::new(),
)
.await
.expect("connect named middleware services");
engine.set_middleware_runner_for_tests(ChainRunner::from_registry(registry));
let generation_guard = engine
.generation_guard(engine.current_generation())
.unwrap();
let ctx = L7EvalContext {
host: "api.example.test".into(),
port,
request_default_port: Some(transport),
policy_name: "passthrough_api".into(),
binary_path: "/usr/bin/curl".into(),
..Default::default()
};

let (mut app, mut relay_client) = tokio::io::duplex(8192);
let (mut relay_upstream, mut upstream) = tokio::io::duplex(8192);
let relay = tokio::spawn(async move {
relay_passthrough_with_credentials(
&mut relay_client,
&mut relay_upstream,
&ctx,
&generation_guard,
Some(engine.as_ref()),
)
.await
});

let request = format!(
"GET /v1/messages HTTP/1.1\r\nHost: api.example.test:{port}\r\nConnection: close\r\n\r\n"
);
app.write_all(request.as_bytes()).await.unwrap();
let mut upstream_request = [0u8; 1024];
tokio::time::timeout(
std::time::Duration::from_secs(1),
upstream.read(&mut upstream_request),
)
.await
.expect("request should reach upstream")
.unwrap();
assert_eq!(*seen.lock().unwrap(), [scheme]);

drop(app);
drop(upstream);
tokio::time::timeout(std::time::Duration::from_secs(1), relay)
.await
.expect("relay should finish")
.unwrap()
.ok();
}
}

#[tokio::test]
async fn websocket_upgrade_request_is_inspected_and_denied() {
// The WebSocket upgrade handshake is an HTTP request the hook can inspect
Expand Down Expand Up @@ -9023,6 +9107,7 @@ network_policies:
let ctx = L7EvalContext {
host: "gateway.example.test".into(),
port: 443,
request_default_port: Some(443),
policy_name: "route_api".into(),
..Default::default()
};
Expand All @@ -9041,10 +9126,34 @@ network_policies:
assert_eq!(event.base().severity, SeverityId::Medium);
assert_eq!(
event.format_shorthand(),
"HTTP:GET [MED] DENIED GET http://gateway.example.test:443/other [policy:route_api engine:l7] [reason:L7_REQUEST deny GET gateway.example.test:443/other reason=no L7 endpoint path matched request]"
"HTTP:GET [MED] DENIED GET https://gateway.example.test/other [policy:route_api engine:l7] [reason:L7_REQUEST deny GET gateway.example.test:443/other reason=no L7 endpoint path matched request]"
);
}

#[test]
fn request_event_url_scheme_follows_transport() {
for (port, transport, scheme, url) in [
(80, 80, "http", "http://api.example.test/v1"),
(443, 443, "https", "https://api.example.test/v1"),
(8443, 443, "https", "https://api.example.test:8443/v1"),
(443, 80, "http", "http://api.example.test:443/v1"),
] {
let ctx = L7EvalContext {
host: "api.example.test".into(),
port,
request_default_port: Some(transport),
policy_name: "api".into(),
..Default::default()
};

let event = build_l7_request_event(&ctx, "GET", "/v1", "allow", "l7", "ok", None);

let json = event.to_json().unwrap();
assert_eq!(json["http_request"]["url"]["scheme"], scheme);
assert!(event.format_shorthand().contains(url), "{url}");
}
}

#[tokio::test]
async fn route_selected_unmatched_path_emits_denied_policy_activity() {
let engine = OpaEngine::from_strings(TEST_POLICY, ENCODED_SLASH_SCOPING_POLICY).unwrap();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use openshell_core::token_grant_failure::{
};
use openshell_ocsf::{
ActionId, ActivityId, DispositionId, Endpoint, HttpActivityBuilder, HttpRequest, SeverityId,
StatusId, Url as OcsfUrl, ctx::ctx as ocsf_ctx, ocsf_emit,
StatusId, ctx::ctx as ocsf_ctx, ocsf_emit,
};
use tracing::warn;

Expand Down Expand Up @@ -345,10 +345,7 @@ fn report_refused_grant(
.disposition(DispositionId::Blocked)
.severity(SeverityId::Medium)
.status(StatusId::Failure)
.http_request(HttpRequest::new(
method,
OcsfUrl::new("http", &ctx.host, request_path, ctx.port),
))
.http_request(HttpRequest::new(method, ctx.ocsf_url(request_path)))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.message(format!(
"Token grant failed for {credential} to {}:{}",
Expand Down Expand Up @@ -407,7 +404,7 @@ async fn inject_selected(
.severity(SeverityId::Informational)
.http_request(HttpRequest::new(
&req.action,
OcsfUrl::new("http", &ctx.host, request_path(&req.target), ctx.port),
ctx.ocsf_url(request_path(&req.target)),
))
.dst_endpoint(Endpoint::from_domain(&ctx.host, ctx.port))
.message(format!(
Expand Down
2 changes: 1 addition & 1 deletion docs/observability/accessing-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The CLI receives logs from the gateway over gRPC. Each line includes a timestamp

```text
[1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
[1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api]
[1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET https://api.github.com/zen [policy:github_api]
[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa]
[1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox
```
Expand Down
6 changes: 3 additions & 3 deletions docs/observability/logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ In the log file, OCSF events appear in a shorthand format with an `OCSF` level l
2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data
2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image
2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa]
2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api engine:opa]
2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET https://api.github.com/zen [policy:github_api engine:opa]
2026-04-01T04:04:32.690Z OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy]
```

Expand Down Expand Up @@ -122,7 +122,7 @@ OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:git
An L7 read-only policy denying a POST:

```text
OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api engine:opa]
OCSF HTTP:POST [MED] DENIED POST https://api.github.com/user/repos [policy:github_api engine:opa]
```

A connection denied because no policy matched:
Expand All @@ -146,7 +146,7 @@ OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy
A supervisor middleware HTTP event records whether it transformed the request. If the middleware also emits a finding, that remains a separate event:

```text
OCSF HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware] [failed:false transformed:true]
OCSF HTTP:POST [INFO] ALLOWED POST https://httpbin.org/anything [policy:httpbin engine:middleware] [failed:false transformed:true]
Comment thread
TaylorMutch marked this conversation as resolved.
OCSF FINDING:CREATE [MED] "configured content matched" [type:content_guard.match count:1 middleware:prototype-content-guard]
```

Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/first-network-policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ openshell logs demo --since 5m --source sandbox
```

```text
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com:443/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy]
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://api.github.com/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy]
```

Policy events are INFO-level log records regardless of their severity, so do not filter them out with `--level warn`. In production, export these events to your SIEM for a complete audit trail of every request your agent makes. Refer to [Logging](/observability/logging) for the event format.
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/github-push-access.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ openshell logs github-demo --since 5m --source sandbox
You should see a denial for a request resembling this one:

```text
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://github.com:443/<org>/<repo>.git/git-receive-pack [policy:_provider_my_github engine:l7] [reason:L7_REQUEST deny POST github.com:443/<org>/<repo>.git/git-receive-pack reason=POST /<org>/<repo>.git/git-receive-pack not permitted by policy]
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://github.com/<org>/<repo>.git/git-receive-pack [policy:_provider_my_github engine:l7] [reason:L7_REQUEST deny POST github.com:443/<org>/<repo>.git/git-receive-pack reason=POST /<org>/<repo>.git/git-receive-pack not permitted by policy]
```

`_provider_my_github` is the rule that OpenShell composes from the attached
Expand Down
2 changes: 1 addition & 1 deletion examples/sandbox-policy-quickstart/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ openshell logs demo --since 5m --source sandbox
```

```text
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com:443/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy]
[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST https://api.github.com/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy]
```

The log captures the exact HTTP method, path, and matching rule. Policy
Expand Down
Loading