Files
OpenShell/crates/openshell-prover/tests/containment_api.rs
Johnny GrecoandKirit93 5bce19ab47 feat(prover): check process, Landlock, and destination IP containment (#3394)
* feat(prover): check process, landlock, and destination IP containment

Signed-off-by: Kirit93 <kthadaka@nvidia.com>
Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* fix(prover): reject ambiguous implicit IP modes

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* fix(prover): validate implicit IP modes policy-wide

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* fix(prover): tighten containment edge handling

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* test(prover): cover boundary v2 evidence shapes

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* docs(prover): document boundary v2 evidence limits

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* refactor(prover): simplify result version contract

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

* refactor(prover): clarify coverage terminology

Signed-off-by: Johnny Greco <jogreco@nvidia.com>

---------

Signed-off-by: Kirit93 <kthadaka@nvidia.com>
Signed-off-by: Johnny Greco <jogreco@nvidia.com>
Co-authored-by: Kirit93 <kthadaka@nvidia.com>
2026-09-18 21:38:08 +00:00

111 lines
3.9 KiB
Rust

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Compile-time and behavioral coverage for the supported external API shape.
use std::time::Duration;
use openshell_prover::containment::{
CheckCoverage, CheckDomain, CheckOptions, CheckResult, Counterexample, Protocol, ReasonCode,
check_within_boundary, parse_policy_str,
};
fn authorize(result: &CheckResult) -> bool {
matches!(result, CheckResult::Within(_))
}
fn result_state(result: &CheckResult) -> &'static str {
// CheckResult is intentionally exhaustive: adding an outcome is a breaking
// API change that requires callers to choose new fail-closed semantics.
match result {
CheckResult::Within(_) => "within",
CheckResult::Exceeds(_) => "exceeds",
CheckResult::Unsupported(_) => "unsupported",
CheckResult::Inconclusive(_) => "inconclusive",
}
}
fn protocol_name(protocol: Protocol) -> &'static str {
match protocol {
Protocol::L4 => "l4",
Protocol::Rest => "rest",
_ => "unknown",
}
}
fn domain_name(domain: CheckDomain) -> &'static str {
match domain {
CheckDomain::Filesystem => "filesystem",
CheckDomain::NetworkL4 => "network_l4",
CheckDomain::NetworkRest => "network_rest",
CheckDomain::Process => "process",
CheckDomain::Landlock => "landlock",
_ => "unknown",
}
}
fn covers(coverage: &CheckCoverage, expected: &str) -> bool {
coverage
.domains
.iter()
.any(|domain| domain_name(*domain) == expected)
}
#[test]
fn external_callers_use_extensible_construction_and_matching_patterns() {
let boundary = parse_policy_str(
"version: 1\nfilesystem_policy: { read_only: [/data], include_workdir: false }\n",
)
.expect("boundary should parse");
let candidate = parse_policy_str(
"version: 1\nfilesystem_policy: { read_write: [/data], include_workdir: false }\n",
)
.expect("candidate should parse");
let mut options = CheckOptions::new(Duration::from_secs(2));
assert_eq!(options.timeout, Duration::from_secs(2));
options.timeout = Duration::from_secs(3);
let result = check_within_boundary(&boundary, &candidate, options);
assert_eq!(result_state(&result), "exceeds");
assert!(!authorize(&result));
let CheckResult::Exceeds(evidence) = &result else {
panic!("expected filesystem violation, got {result:?}");
};
assert!(covers(evidence.coverage(), "filesystem"));
match evidence.counterexample() {
Counterexample::Filesystem { access, path, .. } => {
assert_eq!(access.as_str(), "write");
assert_eq!(path, "/data");
}
Counterexample::Network { protocol, .. } => {
assert_ne!(protocol_name(*protocol), "unknown");
}
_ => panic!("unknown counterexample kinds must fail closed"),
}
}
#[test]
fn external_callers_read_reason_evidence_and_authorize_only_within() {
let policy = parse_policy_str(
"version: 1\nfilesystem_policy: { read_only: [/data], include_workdir: false }\n",
)
.expect("policy should parse");
let within = check_within_boundary(&policy, &policy, CheckOptions::new(Duration::from_secs(1)));
assert!(authorize(&within));
let inconclusive = check_within_boundary(&policy, &policy, CheckOptions::new(Duration::ZERO));
assert!(!authorize(&inconclusive));
let CheckResult::Inconclusive(evidence) = &inconclusive else {
panic!("expected timeout evidence, got {inconclusive:?}");
};
assert_eq!(evidence.reason(), "solver timeout must be positive");
assert_eq!(evidence.reason_code().as_str(), "solver_timeout");
match evidence.reason_code() {
ReasonCode::SolverTimeout => {}
_ => panic!("unexpected reason code"),
}
assert!(covers(evidence.coverage(), "landlock"));
}