Skip to content
Open
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
5 changes: 4 additions & 1 deletion architecture/security-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,10 @@ or decide whether an in-boundary change is eligible for automatic approval.

The containment model covers filesystem paths, supported process identities,
Landlock compatibility requirements, L4 destinations including IP ranges, and
enforced REST method and path authority. Identity comparisons assume consistent
enforced REST method, path, and supported query-parameter authority. Query checks
support exact ASCII values and `*` with the runtime's dot-delimited glob semantics,
including missing and repeated parameters. Other query matchers remain unsupported.
Identity comparisons assume consistent
user and group resolution. Compatibility checks compare requested enforcement
requirements, not the actual kernel state of a running sandbox.
It returns explicit unsupported or inconclusive
Expand Down
7 changes: 6 additions & 1 deletion crates/openshell-prover-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ enum CounterexampleJson<'a> {
protocol: &'a str,
method: Option<&'a str>,
path: Option<&'a str>,
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
query_params: &'a std::collections::BTreeMap<String, Vec<String>>,
},
}

Expand Down Expand Up @@ -413,6 +415,7 @@ fn counterexample_json(counterexample: &Counterexample) -> Result<Counterexample
protocol,
method,
path,
query_params,
..
} => CounterexampleJson::Network {
binary: binary.as_deref(),
Expand All @@ -425,6 +428,7 @@ fn counterexample_json(counterexample: &Counterexample) -> Result<Counterexample
protocol: protocol.as_str(),
method: method.as_deref(),
path: path.as_deref(),
query_params,
},
_ => return Err("unsupported counterexample kind returned by containment API".to_owned()),
};
Expand Down Expand Up @@ -471,9 +475,10 @@ fn render_text(mut writer: impl Write, envelope: &Envelope<'_>) -> Result<(), St
protocol,
method,
path,
query_params,
} => writeln!(
writer,
"counterexample: network binary={} ancestor_binary={} binary_identity_required={} host={}:{} destination_ip={} trusted_gateway={} protocol={} method={} path={}",
"counterexample: network binary={} ancestor_binary={} binary_identity_required={} host={}:{} destination_ip={} trusted_gateway={} protocol={} method={} path={} query_params={query_params:?}",
binary.map_or("-".to_owned(), escape_terminal),
ancestor_binary.map_or("-".to_owned(), escape_terminal),
binary_identity_required,
Expand Down
35 changes: 35 additions & 0 deletions crates/openshell-prover-cli/tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ fn check_json(candidate: &str, boundary: &str) -> Output {
])
}

#[test]
fn query_containment_and_counterexample_are_exposed_in_cli() {
let within = check_json("query-upload.yaml", "query-any.yaml");
assert_eq!(
within.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&within.stdout)
);
let denied = check_json("query-receive.yaml", "query-upload.yaml");
assert_eq!(
denied.status.code(),
Some(1),
"{}",
String::from_utf8_lossy(&denied.stdout)
);
let value: Value = serde_json::from_slice(&denied.stdout).unwrap();
assert_eq!(
value["counterexample"]["query_params"]["service"],
serde_json::json!(["git-receive-pack"])
);
let text = run(&[
"check",
fixture("query-receive.yaml").to_str().unwrap(),
"--boundary",
fixture("query-upload.yaml").to_str().unwrap(),
]);
assert_eq!(text.status.code(), Some(1));
let text = String::from_utf8(text.stdout).unwrap();
assert!(
text.contains("query_params=") && text.contains("git-receive-pack"),
"{text}"
);
}

#[test]
fn help_and_version_succeed() {
for args in [
Expand Down
19 changes: 19 additions & 0 deletions crates/openshell-prover-cli/tests/fixtures/query-any.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

version: 1
network_policies:
git:
binaries:
- path: /usr/bin/git
endpoints:
- host: github.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /org/repo.git/info/refs
query:
service: "*"
19 changes: 19 additions & 0 deletions crates/openshell-prover-cli/tests/fixtures/query-receive.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

version: 1
network_policies:
git:
binaries:
- path: /usr/bin/git
endpoints:
- host: github.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /org/repo.git/info/refs
query:
service: "git-receive-pack"
19 changes: 19 additions & 0 deletions crates/openshell-prover-cli/tests/fixtures/query-upload.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

version: 1
network_policies:
git:
binaries:
- path: /usr/bin/git
endpoints:
- host: github.com
port: 443
protocol: rest
enforcement: enforce
rules:
- allow:
method: GET
path: /org/repo.git/info/refs
query:
service: "git-upload-pack"
13 changes: 12 additions & 1 deletion crates/openshell-prover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,18 @@ endpoint host and path selectors, and REST allow and deny method and path
selectors. It returns `unsupported_policy_shape` when either policy uses a
non-ASCII literal in one of those fields. This boundary does not apply to
filesystem paths or unrelated policy text. Embedded NUL bytes in network
selector fields are also unsupported. ASCII wildcards are modeled over the
selector fields are also unsupported. REST query keys and exact values must be
ASCII without NUL. Supported query matchers are exact strings and `*`; partial
globs and `any` matchers remain unsupported. The runtime treats `.` as a glob
delimiter, so `*` matches an empty value or `a/b`, but not `a.b`. Configured keys
must be present. All repeated values must match an allow constraint; any matching
value satisfies each configured deny constraint. Unconfigured keys are unrestricted.
The model separates wildcard-matching and nonmatching values, including in decoded
`query_params` counterexamples. It does not infer application-specific permissions.
At most 256 query matchers are admitted across both policies; their keys and values
count toward the existing pattern-byte limits.

ASCII wildcards are modeled over the
runtime match language and can therefore match non-ASCII runtime values. A
solver string that cannot be decoded and validated exactly produces
`invalid_witness` rather than counterexample evidence.
Expand Down
Loading
Loading