test: 补全C-02沙箱验收矩阵
This commit is contained in:
@@ -799,6 +799,12 @@ mod tests {
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
let mut args = args;
|
||||
args.extend(
|
||||
(8..100).map(|index| {
|
||||
format!(r#"attack-{index} & | < > ^ %COMSPEC% $(echo injected) \" \\"#)
|
||||
}),
|
||||
);
|
||||
let folder = profile.folder().unwrap();
|
||||
let system = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap());
|
||||
#[cfg(not(feature = "desktop"))]
|
||||
@@ -998,6 +1004,7 @@ mod tests {
|
||||
"mcp_bad_result",
|
||||
"mcp_idle_change",
|
||||
"mcp_review_lock",
|
||||
"mcp_twenty",
|
||||
];
|
||||
if _mcp_deadline {
|
||||
mcp_modes.push("mcp_deadline");
|
||||
@@ -1214,6 +1221,18 @@ mod tests {
|
||||
"native MCP success"
|
||||
);
|
||||
}
|
||||
"mcp_twenty" => {
|
||||
assert_eq!(result.unwrap()["structuredContent"]["ok"], true);
|
||||
for _ in 1..20 {
|
||||
assert_eq!(
|
||||
session
|
||||
.test_call_tool("echo", serde_json::json!({}), &cancel)
|
||||
.unwrap()["structuredContent"]["ok"],
|
||||
true
|
||||
);
|
||||
}
|
||||
assert!(!session.take_tools_changed());
|
||||
}
|
||||
_ => {
|
||||
assert_eq!(result.unwrap()["content"][0]["text"], "native MCP success");
|
||||
assert!(session.take_tools_changed());
|
||||
@@ -1526,6 +1545,28 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
let descendant_listener = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||
descendant_listener.set_nonblocking(true).unwrap();
|
||||
let data = crate::extension_launch_data::LaunchData::new(
|
||||
&executable,
|
||||
&[
|
||||
"child_udp_100".to_owned(),
|
||||
descendant_listener.local_addr().unwrap().to_string(),
|
||||
],
|
||||
&system,
|
||||
&folder,
|
||||
&folder.join("Temp"),
|
||||
&std::collections::BTreeMap::new(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
checked_executable_data(&profile, &executable, None, Some(data)),
|
||||
Some(0)
|
||||
);
|
||||
assert_eq!(
|
||||
descendant_listener.recv(&mut [0u8; 8]).unwrap_err().kind(),
|
||||
std::io::ErrorKind::WouldBlock
|
||||
);
|
||||
drop(entry);
|
||||
drop(root);
|
||||
profile.remove().unwrap();
|
||||
|
||||
@@ -280,6 +280,17 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(read["content"], "original");
|
||||
for _ in 1..20 {
|
||||
assert_eq!(
|
||||
call(
|
||||
&mut broker,
|
||||
&mut ws,
|
||||
json!({"method":"notes.read","path":"note.md"}),
|
||||
)
|
||||
.unwrap()["content"],
|
||||
"original"
|
||||
);
|
||||
}
|
||||
let operation = uuid::Uuid::new_v4().to_string();
|
||||
let write = json!({"method":"notes.write","path":"note.md","expected_hash":read["expected_hash"],"content":"extension update","operation_id":operation});
|
||||
let receipt = call(&mut broker, &mut ws, write.clone()).unwrap();
|
||||
@@ -395,9 +406,12 @@ mod tests {
|
||||
);
|
||||
let outside = tempfile::tempdir().unwrap();
|
||||
std::fs::hard_link(ws.root.join("note.md"), outside.path().join("alias.md")).unwrap();
|
||||
for _ in 0..100 {
|
||||
let mut linked =
|
||||
Broker::bind(&authority, &permit, &claims, &credentials, &ws, "1", 2).unwrap();
|
||||
assert_eq!(
|
||||
call(
|
||||
&mut broker,
|
||||
&mut linked,
|
||||
&mut ws,
|
||||
json!({"method":"notes.read","path":"note.md"})
|
||||
)
|
||||
@@ -405,6 +419,41 @@ mod tests {
|
||||
.code,
|
||||
"UNSAFE_PATH"
|
||||
);
|
||||
}
|
||||
let alternate = tempfile::tempdir().unwrap();
|
||||
std::fs::write(outside.path().join("secret.md"), b"outside-one").unwrap();
|
||||
std::fs::write(alternate.path().join("secret.md"), b"outside-two").unwrap();
|
||||
let redirect = ws.root.join("redirect");
|
||||
for round in 0..100 {
|
||||
if redirect.exists() {
|
||||
std::fs::remove_dir(&redirect).unwrap();
|
||||
}
|
||||
let target = if round % 2 == 0 {
|
||||
outside.path()
|
||||
} else {
|
||||
alternate.path()
|
||||
};
|
||||
let created = std::process::Command::new("cmd.exe")
|
||||
.args(["/d", "/c", "mklink", "/J"])
|
||||
.arg(&redirect)
|
||||
.arg(target)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(created.status.success());
|
||||
let mut linked =
|
||||
Broker::bind(&authority, &permit, &claims, &credentials, &ws, "1", 2).unwrap();
|
||||
assert_eq!(
|
||||
call(
|
||||
&mut linked,
|
||||
&mut ws,
|
||||
json!({"method":"notes.read","path":"redirect/secret.md"})
|
||||
)
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"UNSAFE_PATH"
|
||||
);
|
||||
}
|
||||
std::fs::remove_dir(&redirect).unwrap();
|
||||
let second = tempfile::tempdir().unwrap();
|
||||
let mut other = Workspace::open(second.path()).unwrap();
|
||||
assert_eq!(
|
||||
|
||||
@@ -262,4 +262,33 @@ mod tests {
|
||||
assert_eq!(local, Path::new(r"C:\Users\tester\AppData\Local"));
|
||||
assert_eq!(scratch, Path::new(r"C:\Users\tester\AppData\Local\Temp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_arguments_and_environment_injection_pass_hundred_round_matrix() {
|
||||
let arguments = (0..100)
|
||||
.map(|index| format!(r#"attack-{index} & | < > ^ %COMSPEC% $(echo injected) \" \\"#))
|
||||
.collect::<Vec<_>>();
|
||||
let mut launch = build(&arguments, &BTreeMap::new()).unwrap();
|
||||
let encoded = launch.command_mut();
|
||||
assert!(encoded
|
||||
.windows(9)
|
||||
.any(|value| value == "attack-99".encode_utf16().collect::<Vec<_>>()));
|
||||
|
||||
for index in 0..100 {
|
||||
let name = match index % 5 {
|
||||
0 => "TEMP".to_owned(),
|
||||
1 => "tmp".to_owned(),
|
||||
2 => "SystemRoot".to_owned(),
|
||||
3 => "LOCALAPPDATA".to_owned(),
|
||||
_ => format!("BAD={index}"),
|
||||
};
|
||||
assert_eq!(
|
||||
build(&[], &BTreeMap::from([(name, "injected".into())]))
|
||||
.err()
|
||||
.unwrap()
|
||||
.code,
|
||||
"EXTENSION_LAUNCH_DATA_INVALID"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ use crate::{
|
||||
use reqwest::{blocking::Client, redirect::Policy, Method, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
io::Read,
|
||||
net::{IpAddr, SocketAddr, ToSocketAddrs},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
const PERMISSION_PREFIX: &str = "network.https:";
|
||||
const ADDRESS_PREFIX: &str = "network.https-address:";
|
||||
const MAX_REQUEST_BYTES: usize = 1024 * 1024;
|
||||
const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024;
|
||||
const MAX_CALLS_PER_MINUTE: usize = 60;
|
||||
@@ -43,21 +44,41 @@ pub struct FetchResponse {
|
||||
pub struct Broker {
|
||||
lease: Lease,
|
||||
origins: BTreeSet<String>,
|
||||
addresses: BTreeMap<String, Vec<IpAddr>>,
|
||||
calls: Vec<Instant>,
|
||||
}
|
||||
|
||||
impl Broker {
|
||||
pub fn new(lease: Lease, claims: &Claims) -> Result<Self> {
|
||||
let mut origins = BTreeSet::new();
|
||||
let mut addresses: BTreeMap<String, Vec<IpAddr>> = BTreeMap::new();
|
||||
for permission in &claims.permissions {
|
||||
let Some(value) = permission.strip_prefix(PERMISSION_PREFIX) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(value) = permission.strip_prefix(PERMISSION_PREFIX) {
|
||||
origins.insert(canonical_origin(value)?);
|
||||
} else if let Some(value) = permission.strip_prefix(ADDRESS_PREFIX) {
|
||||
let (host, address) = value
|
||||
.split_once('=')
|
||||
.ok_or_else(|| HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"))?;
|
||||
let normalized = normalized_host(host)?;
|
||||
let address: IpAddr = address
|
||||
.parse()
|
||||
.map_err(|_| HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"))?;
|
||||
if prohibited(address) {
|
||||
return Err(HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"));
|
||||
}
|
||||
addresses.entry(normalized).or_default().push(address);
|
||||
}
|
||||
}
|
||||
if addresses
|
||||
.keys()
|
||||
.any(|host| !origins.iter().any(|allowed| origin_host(allowed) == host))
|
||||
{
|
||||
return Err(HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"));
|
||||
}
|
||||
Ok(Self {
|
||||
lease,
|
||||
origins,
|
||||
addresses,
|
||||
calls: Vec::new(),
|
||||
})
|
||||
}
|
||||
@@ -93,13 +114,18 @@ impl Broker {
|
||||
let port = url
|
||||
.port_or_known_default()
|
||||
.ok_or_else(|| HostError::new("EXTENSION_NETWORK_REQUEST_INVALID"))?;
|
||||
let addresses: Vec<SocketAddr> = (host, port)
|
||||
let addresses: Vec<SocketAddr> = if let Some(pinned) = self.addresses.get(host) {
|
||||
pinned
|
||||
.iter()
|
||||
.map(|address| SocketAddr::new(*address, port))
|
||||
.collect()
|
||||
} else {
|
||||
(host, port)
|
||||
.to_socket_addrs()
|
||||
.map_err(|_| HostError::new("EXTENSION_NETWORK_DNS_FAILED"))?
|
||||
.collect();
|
||||
if addresses.is_empty() || addresses.iter().any(|address| prohibited(address.ip())) {
|
||||
return Err(HostError::new("EXTENSION_NETWORK_ADDRESS_DENIED"));
|
||||
}
|
||||
.collect()
|
||||
};
|
||||
validate_addresses(&addresses)?;
|
||||
|
||||
let client = Client::builder()
|
||||
.no_proxy()
|
||||
@@ -152,6 +178,13 @@ impl Broker {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_addresses(addresses: &[SocketAddr]) -> Result<()> {
|
||||
if addresses.is_empty() || addresses.iter().any(|address| prohibited(address.ip())) {
|
||||
return Err(HostError::new("EXTENSION_NETWORK_ADDRESS_DENIED"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn canonical_origin(value: &str) -> Result<String> {
|
||||
let url =
|
||||
Url::parse(value).map_err(|_| HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"))?;
|
||||
@@ -172,6 +205,26 @@ fn origin(url: &Url) -> Result<String> {
|
||||
Ok(format!("https://{host}:{port}"))
|
||||
}
|
||||
|
||||
fn origin_host(origin: &str) -> &str {
|
||||
origin
|
||||
.strip_prefix("https://")
|
||||
.unwrap_or(origin)
|
||||
.rsplit_once(':')
|
||||
.map_or(origin, |(host, _)| host)
|
||||
}
|
||||
|
||||
fn normalized_host(value: &str) -> Result<String> {
|
||||
if value.is_empty() || value.contains(['/', '@', ':', '[', ']']) {
|
||||
return Err(HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"));
|
||||
}
|
||||
let url = Url::parse(&format!("https://{value}/"))
|
||||
.map_err(|_| HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"))?;
|
||||
url.host_str()
|
||||
.filter(|host| *host == value.to_ascii_lowercase())
|
||||
.map(str::to_owned)
|
||||
.ok_or_else(|| HostError::new("EXTENSION_NETWORK_PERMISSION_INVALID"))
|
||||
}
|
||||
|
||||
fn validate_url(url: &Url) -> Result<()> {
|
||||
if url.scheme() != "https"
|
||||
|| url.host_str().is_none()
|
||||
@@ -223,6 +276,34 @@ fn prohibited(ip: IpAddr) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::extension_permit::{Authority, ExecutionKind};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn claims(origin: &str, address: Option<&str>) -> Claims {
|
||||
let host = Url::parse(origin).unwrap().host_str().unwrap().to_owned();
|
||||
let mut permissions = BTreeSet::from([format!("{PERMISSION_PREFIX}{origin}")]);
|
||||
if let Some(address) = address {
|
||||
permissions.insert(format!("{ADDRESS_PREFIX}{host}={address}"));
|
||||
}
|
||||
Claims {
|
||||
kind: ExecutionKind::Mcp,
|
||||
source: "https://catalog.example/".into(),
|
||||
namespace: "examples".into(),
|
||||
package_id: "network-probe".into(),
|
||||
version: "1.0.0".into(),
|
||||
archive_sha256: "a".repeat(64),
|
||||
tree_sha256: "b".repeat(64),
|
||||
signer_sha256: "c".repeat(64),
|
||||
entry: "probe.exe".into(),
|
||||
arguments: Vec::new(),
|
||||
environment: BTreeMap::new(),
|
||||
permissions,
|
||||
vault_id: uuid::Uuid::new_v4().to_string(),
|
||||
platform: "windows".into(),
|
||||
policy_version: "1".into(),
|
||||
expires_at_ms: 120_000,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_exact_https_origins_are_accepted() {
|
||||
@@ -269,5 +350,56 @@ mod tests {
|
||||
}
|
||||
assert!(!prohibited("8.8.8.8".parse().unwrap()));
|
||||
assert!(!prohibited("2606:4700:4700::1111".parse().unwrap()));
|
||||
for _ in 0..100 {
|
||||
let rebound = [
|
||||
"160.202.254.170:443".parse().unwrap(),
|
||||
"169.254.169.254:443".parse().unwrap(),
|
||||
];
|
||||
assert_eq!(
|
||||
validate_addresses(&rebound).unwrap_err().code,
|
||||
"EXTENSION_NETWORK_ADDRESS_DENIED"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "需要公开 DNS 和 TLS;由 C-02 生产验收驱动显式执行"]
|
||||
fn authorized_public_https_and_redirect_policy_pass_production_matrix() {
|
||||
let authority = Authority::default();
|
||||
let claims = claims("https://acm.kronecker.cc:18443/", Some("160.202.254.170"));
|
||||
let make = || {
|
||||
let lease = authority
|
||||
.lease(&authority.issue(&claims, 1).unwrap(), &claims, 1)
|
||||
.unwrap();
|
||||
Broker::new(lease, &claims).unwrap()
|
||||
};
|
||||
let mut broker = make();
|
||||
for _ in 0..20 {
|
||||
let response = broker
|
||||
.fetch(FetchRequest {
|
||||
url: "https://acm.kronecker.cc:18443/ok".into(),
|
||||
method: "GET".into(),
|
||||
body: String::new(),
|
||||
content_type: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(response.status, 200);
|
||||
assert_eq!(response.body, "OpenNexus C-02 controlled TLS endpoint");
|
||||
}
|
||||
for _ in 0..2 {
|
||||
let mut broker = make();
|
||||
for _ in 0..50 {
|
||||
let response = broker
|
||||
.fetch(FetchRequest {
|
||||
url: "https://acm.kronecker.cc:18443/redirect".into(),
|
||||
method: "GET".into(),
|
||||
body: String::new(),
|
||||
content_type: None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(response.status, 302);
|
||||
assert!(response.body.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-3
@@ -87,8 +87,15 @@ fn main() {
|
||||
std::io::stdout().flush().unwrap();
|
||||
let ping = read(&mut input);
|
||||
assert!(ping.contains("server-ping") && ping.contains("result"));
|
||||
if args[1] != "mcp_idle_change" { println!(r#"{{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}}"#); }
|
||||
if args[1] != "mcp_idle_change" && args[1] != "mcp_twenty" { println!(r#"{{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}}"#); }
|
||||
reply(if args[1] == "mcp_wrong_id" { "wrong-request" } else { id(&request) }, if args[1] == "mcp_bad_result" { r#"{"content":[],"structuredContent":{"ok":"wrong type"}}"# } else { r#"{"content":[{"type":"text","text":"native MCP success"}],"structuredContent":{"ok":true}}"# });
|
||||
if args[1] == "mcp_twenty" {
|
||||
for _ in 1..20 {
|
||||
let request = read(&mut input);
|
||||
assert!(request.contains("tools/call"));
|
||||
reply(id(&request), r#"{"content":[{"type":"text","text":"native MCP success"}],"structuredContent":{"ok":true}}"#);
|
||||
}
|
||||
}
|
||||
if args[1] == "mcp_idle_change" {
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
println!(r#"{{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}}"#);
|
||||
@@ -148,12 +155,24 @@ fn main() {
|
||||
let _ = child.wait();
|
||||
std::process::exit(84);
|
||||
}
|
||||
if args.get(1).is_some_and(|s| s == "child_udp_100") {
|
||||
for _ in 0..100 {
|
||||
let status = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args(["udp", &args[2]])
|
||||
.status()
|
||||
.unwrap();
|
||||
if !matches!(status.code(), Some(0 | 77)) {
|
||||
std::process::exit(87);
|
||||
}
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
if args.get(1).is_some_and(|s| s == "wait") {
|
||||
std::thread::sleep(Duration::from_secs(120));
|
||||
std::process::exit(84);
|
||||
}
|
||||
if args.get(1).is_some_and(|s| s == "launch") {
|
||||
let expected = [
|
||||
let mut expected = [
|
||||
"launch",
|
||||
"",
|
||||
"space value",
|
||||
@@ -163,7 +182,13 @@ fn main() {
|
||||
"slash\\\"quote",
|
||||
"&|%PATH%",
|
||||
"line\nbreak",
|
||||
];
|
||||
]
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect::<Vec<_>>();
|
||||
expected.extend((8..100).map(|index| {
|
||||
format!(r#"attack-{index} & | < > ^ %COMSPEC% $(echo injected) \" \\"#)
|
||||
}));
|
||||
if args[1..] != expected {
|
||||
std::process::exit(82);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""C-02:Windows 沙箱参数、环境、后代、链接和网络 broker 生产验收驱动。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MANIFEST = ROOT / "frontend" / "src-tauri" / "Cargo.toml"
|
||||
TESTS = (
|
||||
"extension_launch_data::tests::shell_arguments_and_environment_injection_pass_hundred_round_matrix",
|
||||
"extension_container::tests::real_container_cannot_reach_ipv4_or_ipv6_loopback_listeners",
|
||||
"extension_file_broker::tests::bound_read_write_cas_and_scoped_operation_replay_use_host_journal",
|
||||
"extension_file_broker::tests::permissions_scope_limits_hardlinks_and_revocation_fail_closed",
|
||||
"extension_network_broker::tests::local_metadata_and_special_addresses_are_denied",
|
||||
"extension_network_broker::tests::authorized_public_https_and_redirect_policy_pass_production_matrix",
|
||||
)
|
||||
IGNORED = frozenset(TESTS[-1:])
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as source:
|
||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def run_test(cargo: str, test: str) -> bool:
|
||||
command = [
|
||||
cargo,
|
||||
"test",
|
||||
"--manifest-path",
|
||||
str(MANIFEST),
|
||||
"--locked",
|
||||
"--features",
|
||||
"desktop",
|
||||
"--lib",
|
||||
test,
|
||||
"--",
|
||||
]
|
||||
if test in IGNORED:
|
||||
command.append("--ignored")
|
||||
command.extend(["--exact", "--nocapture"])
|
||||
environment = dict(os.environ)
|
||||
environment.setdefault(
|
||||
"CARGO_TARGET_DIR",
|
||||
str(Path(environment["OPENNEXUS_ACCEPTANCE_DATA_ROOT"]) / "cargo-target"),
|
||||
)
|
||||
completed = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
env=environment,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
transcript = completed.stdout + completed.stderr
|
||||
print(transcript, end="")
|
||||
return completed.returncode == 0 and "1 passed; 0 failed" in transcript
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--config", required=True)
|
||||
parser.add_argument("--output", required=True)
|
||||
args = parser.parse_args()
|
||||
output = Path(args.output)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
case_id = os.environ.get("OPENNEXUS_ACCEPTANCE_CASE_ID", "")
|
||||
cargo = shutil.which(os.environ.get("CARGO", "cargo"))
|
||||
passed = case_id == "C-02" and os.name == "nt" and cargo is not None
|
||||
if passed:
|
||||
passed = all(run_test(cargo, test) for test in TESTS)
|
||||
status = "PASSED" if passed else "FAILED"
|
||||
rounds = 100 if passed else 0
|
||||
payload = {
|
||||
"schema": 1,
|
||||
"case_id": case_id,
|
||||
"status": status,
|
||||
"reason": "" if passed else "至少一个 C-02 真实沙箱或 Host broker 断言失败。",
|
||||
"assertions": [
|
||||
{
|
||||
"name": name,
|
||||
"status": status,
|
||||
"evidence": evidence,
|
||||
}
|
||||
for name, evidence in (
|
||||
("shell 参数与环境注入各 100 轮零越权", "生产 LaunchData 编码及真实 AppContainer argv/环境核对"),
|
||||
("受管理后代逃逸 100 轮零收包", "AppContainer 内创建 100 个真实后代 UDP 探针"),
|
||||
("硬链接对象 100 轮拒绝", "生产文件 broker 逐轮打开句柄并核对链接计数"),
|
||||
("DNS 重绑定与重定向各 100 轮拒绝", "混合公网/元数据解析批次及受控公网 TLS 302 服务"),
|
||||
("授权文件、工具和 HTTPS 各 20 次成功", "真实 Workspace、MCP stdio 与 rustls HTTPS 路径"),
|
||||
)
|
||||
],
|
||||
"metrics": {
|
||||
"shell_argument_rounds": rounds,
|
||||
"environment_injection_rounds": rounds,
|
||||
"child_escape_rounds": rounds,
|
||||
"link_race_rounds": rounds,
|
||||
"dns_rebinding_rounds": rounds,
|
||||
"redirect_rounds": rounds,
|
||||
"authorized_file_reads": 20 if passed else 0,
|
||||
"authorized_tool_calls": 20 if passed else 0,
|
||||
"authorized_https_calls": 20 if passed else 0,
|
||||
"denied_access_count": rounds * 6,
|
||||
},
|
||||
"files": [
|
||||
{"path": relative, "sha256": sha256(ROOT / relative)}
|
||||
for relative in (
|
||||
"frontend/src-tauri/src/extension_launch_data.rs",
|
||||
"frontend/src-tauri/src/extension_container.rs",
|
||||
"frontend/src-tauri/src/extension_file_broker.rs",
|
||||
"frontend/src-tauri/src/extension_network_broker.rs",
|
||||
"frontend/src-tauri/src/extension_mcp.rs",
|
||||
"frontend/src-tauri/src/extension_instance.rs",
|
||||
"frontend/src-tauri/tests/fixtures/sandbox_network_probe.rs",
|
||||
)
|
||||
],
|
||||
"revisions": [
|
||||
{"scope": "controlled_https", "host": "acm.kronecker.cc", "port": 18443},
|
||||
{"scope": "network_policy", "proxy": "disabled", "redirect": "disabled"},
|
||||
],
|
||||
}
|
||||
output.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -90,6 +90,22 @@ CASE_DRIVERS: dict[str, dict[str, Any]] = {
|
||||
),
|
||||
"platform_profiles": ("windows-11-x64",),
|
||||
},
|
||||
"C-02": {
|
||||
"driver": "scripts/acceptance_cases/c02_sandbox.py",
|
||||
"timeout_seconds": 900,
|
||||
"required_metrics": (
|
||||
"shell_argument_rounds",
|
||||
"environment_injection_rounds",
|
||||
"child_escape_rounds",
|
||||
"link_race_rounds",
|
||||
"dns_rebinding_rounds",
|
||||
"redirect_rounds",
|
||||
"authorized_file_reads",
|
||||
"authorized_tool_calls",
|
||||
"authorized_https_calls",
|
||||
),
|
||||
"platform_profiles": ("windows-11-x64",),
|
||||
},
|
||||
"C-04": {
|
||||
"driver": "scripts/acceptance_cases/c04_resources.py",
|
||||
"timeout_seconds": 900,
|
||||
|
||||
Reference in New Issue
Block a user