use plecto_host::test_support::{TestSigner, bound_sbom, filter_quickstart_component};
use plecto_host::{
Host, HttpRequest, HttpResponse, Isolation, LoadError, LoadOptions, RequestBodyDecision,
RequestTrace, ResponseDecision, SignedArtifact, TrustPolicy,
};
fn component_bytes() -> Vec<u8> {
std::fs::read(env!("FILTER_HELLO_COMPONENT")).expect("read filter-hello component")
}
struct Fixture {
signer: TestSigner,
bytes: Vec<u8>,
component_signature: Vec<u8>,
sbom: Vec<u8>,
sbom_signature: Vec<u8>,
}
fn fixture() -> Fixture {
let bytes = component_bytes();
let signer = TestSigner::new().unwrap();
let component_signature = signer.sign(&bytes).unwrap();
let sbom = bound_sbom(&bytes);
let sbom_signature = signer.sign(&sbom).unwrap();
Fixture {
signer,
bytes,
component_signature,
sbom,
sbom_signature,
}
}
impl Fixture {
fn artifact(&self) -> SignedArtifact<'_> {
SignedArtifact {
component_bytes: &self.bytes,
component_signature: &self.component_signature,
sbom: &self.sbom,
sbom_signature: &self.sbom_signature,
}
}
fn host(&self) -> Host {
Host::new(self.signer.trust_policy().unwrap()).unwrap()
}
}
#[test]
fn component_satisfies_plecto_filter_world() {
let fx = fixture();
fx.host()
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.expect("filter-hello must satisfy plecto:filter@0.3.0 and pass the provenance gate");
}
#[test]
fn component_loads_under_both_isolation_modes() {
let fx = fixture();
let host = fx.host();
let trusted = host
.load("filter-hello", &fx.artifact(), LoadOptions::trusted())
.expect("pooling (trusted) engine must instantiate the component");
assert_eq!(trusted.isolation(), Isolation::Trusted);
let untrusted = host
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.expect("on-demand (untrusted) engine must instantiate the component");
assert_eq!(untrusted.isolation(), Isolation::Untrusted);
}
#[test]
fn response_hook_is_honoured() {
let fx = fixture();
let filter = fx
.host()
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.unwrap();
let resp = HttpResponse {
status: 200,
headers: vec![],
body: vec![],
};
let req = HttpRequest {
method: "GET".to_string(),
path: "/".to_string(),
authority: "example.test".to_string(),
scheme: "https".to_string(),
headers: vec![],
};
let (decision, _logs) = filter
.on_response(&req, &resp, &RequestTrace::root())
.unwrap();
assert!(matches!(decision, ResponseDecision::Continue));
}
#[test]
fn load_rejects_signature_from_untrusted_key() {
let fx = fixture();
let other_key = TestSigner::new().unwrap();
let host = Host::new(other_key.trust_policy().unwrap()).unwrap();
match host.load("filter-hello", &fx.artifact(), LoadOptions::untrusted()) {
Ok(_) => panic!("a signature from an untrusted key must be rejected (fail-closed)"),
Err(e) => {
assert!(
e.to_string().contains("component signature"),
"rejection reason should name the component signature, got: {e}"
);
assert!(
matches!(
e.downcast_ref::<LoadError>(),
Some(LoadError::UnverifiedComponentSignature)
),
"expected LoadError::UnverifiedComponentSignature, got: {e:?}"
);
}
}
}
#[test]
fn load_rejects_tampered_component() {
let fx = fixture();
let host = fx.host();
let mut tampered = fx.bytes.clone();
*tampered.last_mut().unwrap() ^= 0xff;
let artifact = SignedArtifact {
component_bytes: &tampered,
component_signature: &fx.component_signature,
sbom: &fx.sbom,
sbom_signature: &fx.sbom_signature,
};
assert!(
host.load("filter-hello", &artifact, LoadOptions::untrusted())
.is_err(),
"tampered component bytes must fail signature verification (fail-closed)"
);
}
#[test]
fn load_rejects_missing_sbom() {
let fx = fixture();
let host = fx.host();
let empty_sbom_sig = fx.signer.sign(b"").unwrap();
let artifact = SignedArtifact {
component_bytes: &fx.bytes,
component_signature: &fx.component_signature,
sbom: b"",
sbom_signature: &empty_sbom_sig,
};
match host.load("filter-hello", &artifact, LoadOptions::untrusted()) {
Ok(_) => panic!("a missing SBOM must be rejected (fail-closed)"),
Err(e) => {
assert!(
e.to_string().contains("SBOM"),
"rejection reason should name the SBOM, got: {e}"
);
assert!(
matches!(e.downcast_ref::<LoadError>(), Some(LoadError::MissingSbom)),
"expected LoadError::MissingSbom, got: {e:?}"
);
}
}
}
#[test]
fn load_rejects_bad_sbom_signature() {
let fx = fixture();
let other = TestSigner::new().unwrap();
let bad_sbom_sig = other.sign(&fx.sbom).unwrap();
let host = fx.host();
let artifact = SignedArtifact {
component_bytes: &fx.bytes,
component_signature: &fx.component_signature,
sbom: &fx.sbom,
sbom_signature: &bad_sbom_sig,
};
match host.load("filter-hello", &artifact, LoadOptions::untrusted()) {
Ok(_) => panic!("an SBOM signature from an untrusted key must be rejected"),
Err(e) => {
assert!(
e.to_string().contains("SBOM signature"),
"rejection reason should name the SBOM signature, got: {e}"
);
assert!(
matches!(
e.downcast_ref::<LoadError>(),
Some(LoadError::UnverifiedSbomSignature)
),
"expected LoadError::UnverifiedSbomSignature, got: {e:?}"
);
}
}
}
#[test]
fn load_rejects_sbom_for_other_component() {
let fx = fixture();
let other_component = b"a different component".to_vec();
let other_sbom = bound_sbom(&other_component);
let other_sbom_sig = fx.signer.sign(&other_sbom).unwrap();
let artifact = SignedArtifact {
component_bytes: &fx.bytes,
component_signature: &fx.component_signature,
sbom: &other_sbom,
sbom_signature: &other_sbom_sig,
};
match fx
.host()
.load("filter-hello", &artifact, LoadOptions::untrusted())
{
Ok(_) => panic!("an SBOM attesting a different component must be rejected"),
Err(e) => {
assert!(
e.to_string().contains("does not attest this component"),
"rejection reason should name the binding failure, got: {e}"
);
assert!(
matches!(e.downcast_ref::<LoadError>(), Some(LoadError::SbomNotBound)),
"expected LoadError::SbomNotBound, got: {e:?}"
);
}
}
}
#[test]
fn empty_trust_policy_loads_nothing() {
let fx = fixture();
let host = Host::new(TrustPolicy::empty()).unwrap();
assert!(
host.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.is_err(),
"an empty trust policy must load nothing (deny-by-default)"
);
}
#[test]
fn load_rejects_empty_filter_id() {
let fx = fixture();
match fx.host().load("", &fx.artifact(), LoadOptions::untrusted()) {
Ok(_) => panic!("an empty filter id must be rejected"),
Err(e) => {
assert!(
e.to_string().contains("filter id"),
"rejection should name the filter id, got: {e}"
);
assert!(
matches!(
e.downcast_ref::<LoadError>(),
Some(LoadError::EmptyFilterId)
),
"expected LoadError::EmptyFilterId, got: {e:?}"
);
}
}
}
#[test]
fn load_rejects_filter_id_with_namespace_delimiter() {
let fx = fixture();
let evil_id = format!("a{}b", '\u{1f}');
match fx
.host()
.load(&evil_id, &fx.artifact(), LoadOptions::untrusted())
{
Ok(_) => panic!("a filter id containing the KV namespace delimiter must be rejected"),
Err(e) => {
assert!(
e.to_string().contains("delimiter"),
"rejection should name the delimiter, got: {e}"
);
assert!(
matches!(
e.downcast_ref::<LoadError>(),
Some(LoadError::FilterIdContainsDelimiter)
),
"expected LoadError::FilterIdContainsDelimiter, got: {e:?}"
);
}
}
}
#[test]
fn request_body_hook_transforms_then_continues() {
let fx = fixture();
let filter = fx
.host()
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.unwrap();
let (decision, _logs) = filter
.on_request_body(b"hello world", &RequestTrace::root())
.unwrap();
match decision {
RequestBodyDecision::Continue(body) => assert_eq!(body, b"HELLO WORLD".to_vec()),
RequestBodyDecision::ShortCircuit(_) => panic!("expected continue with transformed body"),
}
}
#[test]
fn request_body_hook_short_circuits_before_upstream() {
let fx = fixture();
let filter = fx
.host()
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.unwrap();
let (decision, _logs) = filter
.on_request_body(b"please deny-body now", &RequestTrace::root())
.unwrap();
match decision {
RequestBodyDecision::ShortCircuit(resp) => assert_eq!(resp.status, 403),
RequestBodyDecision::Continue(_) => panic!("expected short-circuit 403 on the marker body"),
}
}
#[test]
fn body_reading_filter_reports_reads_body() {
let fx = fixture();
let filter = fx
.host()
.load("filter-hello", &fx.artifact(), LoadOptions::untrusted())
.unwrap();
assert!(
filter.reads_body(),
"a filter exporting on-request-body must report reads_body() == true"
);
}
#[test]
fn header_only_filter_reports_no_body_read_and_never_inspects_it() {
let signer = TestSigner::new().unwrap();
let bytes = filter_quickstart_component();
let component_signature = signer.sign(&bytes).unwrap();
let sbom = bound_sbom(&bytes);
let sbom_signature = signer.sign(&sbom).unwrap();
let artifact = SignedArtifact {
component_bytes: &bytes,
component_signature: &component_signature,
sbom: &sbom,
sbom_signature: &sbom_signature,
};
let host = Host::new(signer.trust_policy().unwrap()).unwrap();
let filter = host
.load("filter-quickstart", &artifact, LoadOptions::untrusted())
.unwrap();
assert!(
!filter.reads_body(),
"a header-only filter must report reads_body() == false"
);
let (decision, _logs) = filter
.on_request_body(b"contains deny-body marker", &RequestTrace::root())
.unwrap();
match decision {
RequestBodyDecision::Continue(body) => {
assert_eq!(body, b"contains deny-body marker".to_vec());
}
RequestBodyDecision::ShortCircuit(_) => {
panic!("a header-only filter must not inspect or short-circuit on the body")
}
}
}