pub mod ack;
pub mod ceiling;
pub mod diff;
pub mod model;
pub mod render;
pub mod verify;
use ack::Acknowledgment;
use diff::Finding;
use model::{ManifestError, PostureManifest};
use render::Report;
pub const EXIT_BLOCKED: i32 = 1;
pub const EXIT_USAGE: i32 = 2;
#[derive(Debug, Clone)]
pub struct DiffOptions<'a> {
pub base: &'a str,
pub head: &'a str,
pub format: &'a str,
pub output: Option<&'a str>,
pub acks: &'a [String],
pub ack_file: Option<&'a str>,
pub ack_dir: Option<&'a str>,
pub allow_missing_base: bool,
}
#[derive(Debug)]
pub struct Evaluation {
pub findings: Vec<Finding>,
pub ack_digest: String,
pub marker: String,
pub head_posture_digest: String,
pub acknowledged: Option<Acknowledgment>,
pub bootstrap: bool,
}
impl Evaluation {
#[must_use]
pub fn blocked(&self) -> bool {
!diff::widening(&self.findings).is_empty() && self.acknowledged.is_none()
}
#[must_use]
pub fn exit_code(&self) -> i32 {
i32::from(self.blocked()) * EXIT_BLOCKED
}
fn report(&self) -> Report<'_> {
Report {
findings: &self.findings,
ack_digest: self.ack_digest.clone(),
marker: self.marker.clone(),
head_posture_digest: self.head_posture_digest.clone(),
acknowledged: self.acknowledged.clone(),
bootstrap: self.bootstrap,
}
}
}
#[must_use]
pub fn evaluate(
base: Option<&PostureManifest>,
head: &PostureManifest,
acks: &[Acknowledgment],
) -> Evaluation {
let findings = base.map(|b| diff::diff(b, head)).unwrap_or_default();
let widening = diff::widening(&findings);
let ack_digest = ack::ack_digest(&widening);
let cover = ceiling::cover(&widening, head);
let acknowledged = if widening.is_empty() {
None
} else {
ack::matching(acks, &ack_digest, &cover).cloned()
};
let marker = cover.marker(&ack_digest);
Evaluation {
findings,
ack_digest,
marker,
head_posture_digest: head.posture_digest(),
acknowledged,
bootstrap: base.is_none(),
}
}
pub fn render(evaluation: &Evaluation, format: &str) -> Result<String, String> {
let report = evaluation.report();
match format {
"markdown" | "md" => Ok(render::markdown(&report)),
"text" => Ok(render::text(&report)),
"json" => Ok(render::json(&report)),
other => Err(format!(
"unknown --format `{other}` (expected `markdown`, `text`, or `json`)"
)),
}
}
fn harvested_acks(opts: &DiffOptions<'_>) -> Result<Vec<Acknowledgment>, String> {
let mut acks = Vec::new();
for ack in opts.acks {
acks.extend(ack::parse_comment(&format!(
"{} {}",
ack::ACK_PHRASE,
ack.trim()
)));
}
if let Some(path) = opts.ack_file {
let harvested = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read acknowledgment file {path}: {e}"))?;
acks.extend(ack::parse_acks(&harvested));
}
if let Some(dir) = opts.ack_dir {
let read_error =
|e: std::io::Error| format!("cannot read acknowledgment directory {dir}: {e}");
let mut files = Vec::new();
for entry in std::fs::read_dir(dir).map_err(read_error)? {
let entry = entry.map_err(read_error)?;
if entry.file_type().map_err(read_error)?.is_file() {
files.push(entry.path());
}
}
files.sort();
for file in files {
let bytes = std::fs::read(&file)
.map_err(|e| format!("cannot read acknowledgment file {}: {e}", file.display()))?;
acks.extend(ack::parse_comment(&String::from_utf8_lossy(&bytes)));
}
}
Ok(acks)
}
#[must_use]
pub fn run_diff(opts: &DiffOptions<'_>) -> i32 {
let head = match PostureManifest::read(opts.head) {
Ok(m) => m,
Err(e) => return fail(&e.to_string()),
};
let base = match PostureManifest::read(opts.base) {
Ok(m) => Some(m),
Err(ManifestError::Io { ref source, .. })
if opts.allow_missing_base && source.kind() == std::io::ErrorKind::NotFound =>
{
None
}
Err(e) => return fail(&e.to_string()),
};
let acks = match harvested_acks(opts) {
Ok(a) => a,
Err(e) => return fail(&e),
};
if let Some(base_manifest) = &base
&& !base_manifest.dimensions.routes.entries.is_empty()
&& head.dimensions.routes.entries.is_empty()
{
return fail(&format!(
"{} lists no routes at all while {} lists {} \u{2014} refusing to score that as a \
narrowing. Rebuild the manifest with `autumn routes audit --manifest {}` and \
the build flags of the base manifest.",
opts.head,
opts.base,
base_manifest.dimensions.routes.entries.len(),
opts.head
));
}
let evaluation = evaluate(base.as_ref(), &head, &acks);
let rendered = match render(&evaluation, opts.format) {
Ok(r) => r,
Err(e) => return fail(&e),
};
if let Some(path) = opts.output
&& let Err(e) = std::fs::write(path, &rendered)
{
return fail(&format!("cannot write report to {path}: {e}"));
}
if !rendered.is_empty() {
println!("{rendered}");
}
if evaluation.blocked() {
eprintln!(
"\n\u{2717} security posture widened and is not acknowledged.\n \
Acknowledge it with this line \u{2014} as a pull-request comment, or \
locally as `--ack '<text after the phrase>'`:\n\n {} {}\n",
ack::ACK_PHRASE,
evaluation.marker
);
}
evaluation.exit_code()
}
#[must_use]
pub fn run_digest(manifest: &str, format: &str) -> i32 {
let digest = match verify::manifest_digest(manifest) {
Ok(d) => d,
Err(e) => return fail(&e.to_string()),
};
match format {
"text" => println!("{digest}"),
"json" => println!(
"{}",
serde_json::json!({ "manifest": manifest, "posture_digest": digest })
),
other => {
return fail(&format!(
"unknown --format `{other}` (expected `text` or `json`)"
));
}
}
0
}
#[must_use]
pub fn run_verify(opts: &verify::VerifyOptions<'_>) -> i32 {
let report = match verify::verify_with(opts, &verify::GhAttestationVerifier) {
Ok(r) => r,
Err(e) => return fail(&e.to_string()),
};
println!("\u{1F342} autumn routes posture verify\n");
println!(" manifest {}", report.manifest);
println!(" posture digest {}", report.posture_digest);
for check in &report.checks {
let mark = if check.waived {
"\u{26A0}"
} else if check.passed {
"\u{2713}"
} else {
"\u{2717}"
};
println!(" {mark} {:<22} {}", check.name, check.detail);
}
if report.passed() { 0 } else { EXIT_BLOCKED }
}
fn fail(message: &str) -> i32 {
eprintln!("\u{2717} {message}");
EXIT_USAGE
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(json: &str) -> PostureManifest {
PostureManifest::parse(json, "test.json").expect("fixture parses")
}
fn manifest_json(routes: &str, build: &str) -> String {
format!(
r#"{{"schema_version":5,{build}"dimensions":{{
"routes":{{"provenance":"provable","source":"m","entries":[{routes}]}}
}},"excluded":[]}}"#
)
}
fn manifest(routes: &str) -> PostureManifest {
parse(&format!(
r#"{{"schema_version":3,"dimensions":{{
"routes":{{"provenance":"provable","source":"m","entries":[{routes}]}},
"csrf":{{"provenance":"declared","source":"c","exempt_paths":[],"entries":[]}},
"security_headers":{{"provenance":"declared","source":"c","entries":[]}},
"authorization_policies":{{"provenance":"provable","source":"m","runtime_caveat":"x","entries":[]}}
}},"excluded":[]}}"#
))
}
fn route(path: &str, classification: &str, roles: &[&str]) -> String {
let roles = roles
.iter()
.map(|r| format!("\"{r}\""))
.collect::<Vec<_>>()
.join(",");
format!(
r#"{{"path":"{path}","method":"GET","name":"h","classification":"{classification}",
"roles":[{roles}],"scopes":[],"policy":false,"source":"user","provenance":"provable"}}"#
)
}
#[test]
fn an_unchanged_posture_neither_blocks_nor_says_anything() {
let base = manifest(&route("/admin", "gated", &["admin"]));
let head = manifest(&route("/admin", "gated", &["admin"]));
let e = evaluate(Some(&base), &head, &[]);
assert!(!e.blocked());
assert_eq!(e.exit_code(), 0);
assert!(e.findings.is_empty());
assert_eq!(render(&e, "markdown").unwrap(), "");
}
#[test]
fn a_widening_blocks_until_the_matching_marker_is_present() {
let base = manifest(&route("/admin", "gated", &["admin"]));
let head = manifest(&route("/admin", "public", &[]));
let blocked = evaluate(Some(&base), &head, &[]);
assert!(blocked.blocked());
assert_eq!(blocked.exit_code(), EXIT_BLOCKED);
let comment = format!("{} {}", ack::ACK_PHRASE, ack::short(&blocked.ack_digest));
let acknowledged = evaluate(Some(&base), &head, &ack::parse_acks(&comment));
assert!(!acknowledged.blocked());
assert_eq!(acknowledged.exit_code(), 0);
assert!(acknowledged.acknowledged.is_some());
}
#[test]
fn re_widening_after_an_acknowledgment_blocks_again() {
let base = manifest(&route("/admin", "gated", &["admin"]));
let first = manifest(&route("/admin", "public", &[]));
let comment = format!(
"{} {}",
ack::ACK_PHRASE,
ack::short(&evaluate(Some(&base), &first, &[]).ack_digest)
);
assert!(!evaluate(Some(&base), &first, &ack::parse_acks(&comment)).blocked());
let second = manifest(&format!(
"{},{}",
route("/admin", "public", &[]),
route("/internal", "public", &[])
));
assert!(
evaluate(Some(&base), &second, &ack::parse_acks(&comment)).blocked(),
"a new widening must not inherit the old acknowledgment"
);
}
#[test]
fn an_unrelated_push_keeps_the_acknowledgment_valid() {
let base = manifest(&route("/admin", "gated", &["admin"]));
let first = manifest(&route("/admin", "public", &[]));
let comment = format!(
"{} {}",
ack::ACK_PHRASE,
ack::short(&evaluate(Some(&base), &first, &[]).ack_digest)
);
let later = manifest(&format!(
"{},{}",
route("/admin", "public", &[]),
route("/reports", "gated", &["admin"])
));
let e = evaluate(Some(&base), &later, &ack::parse_acks(&comment));
assert!(!e.blocked());
assert!(
e.acknowledged.is_some(),
"it passes *because* the acknowledgment still matches, not because \
the widening stopped being detected"
);
assert_eq!(diff::widening(&e.findings).len(), 1);
}
#[test]
fn narrowing_only_changes_never_block() {
let base = manifest(&route("/admin", "public", &[]));
let head = manifest(&route("/admin", "gated", &["admin"]));
let e = evaluate(Some(&base), &head, &[]);
assert!(!e.blocked());
assert!(!e.findings.is_empty(), "but they are still reported");
}
#[test]
fn with_no_baseline_nothing_blocks() {
let head = manifest(&route("/admin", "public", &[]));
let e = evaluate(None, &head, &[]);
assert!(e.bootstrap);
assert!(!e.blocked());
assert!(e.findings.is_empty());
assert_eq!(e.exit_code(), 0);
}
#[test]
fn the_head_posture_digest_is_the_manifests_own() {
let head = manifest(&route("/admin", "gated", &["admin"]));
let e = evaluate(None, &head, &[]);
assert_eq!(e.head_posture_digest, head.posture_digest());
}
#[test]
fn an_unknown_format_is_a_usage_error_not_a_silent_fallback() {
let head = manifest(&route("/a", "gated", &["admin"]));
let e = evaluate(None, &head, &[]);
assert!(render(&e, "markdwon").is_err());
}
#[test]
fn an_unreadable_base_is_a_usage_error_even_with_allow_missing_base() {
let dir = tempfile::tempdir().unwrap();
let head = dir.path().join("head.json");
std::fs::write(
&head,
serde_json::to_string(&serde_json::json!({
"schema_version": 3,
"dimensions": {"routes": {"entries": [
{"path": "/a", "method": "GET", "classification": "public"}
]}}
}))
.unwrap(),
)
.unwrap();
let base = dir.path().join("base-dir");
std::fs::create_dir(&base).unwrap();
let code = run_diff(&DiffOptions {
base: base.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "json",
output: None,
acks: &[],
ack_file: None,
ack_dir: None,
allow_missing_base: true,
});
assert_eq!(code, EXIT_USAGE, "must not be scored as a clean bootstrap");
}
#[test]
fn an_empty_head_manifest_against_a_populated_base_is_a_usage_error() {
let dir = tempfile::tempdir().unwrap();
let base = dir.path().join("base.json");
let head = dir.path().join("head.json");
std::fs::write(
&base,
serde_json::to_string(&serde_json::json!({
"schema_version": 3,
"dimensions": {"routes": {"entries": [
{"path": "/a", "method": "GET", "classification": "gated", "roles": ["admin"]}
]}}
}))
.unwrap(),
)
.unwrap();
std::fs::write(&head, r#"{"schema_version":3}"#).unwrap();
let code = run_diff(&DiffOptions {
base: base.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "json",
output: None,
acks: &[],
ack_file: None,
ack_dir: None,
allow_missing_base: true,
});
assert_eq!(code, EXIT_USAGE);
}
#[test]
fn the_output_flag_writes_the_report_and_writes_nothing_when_clean() {
let dir = tempfile::tempdir().unwrap();
let base = dir.path().join("base.json");
let head = dir.path().join("head.json");
let report = dir.path().join("report.md");
let write = |path: &std::path::Path, classification: &str, roles: &str| {
std::fs::write(
path,
format!(
r#"{{"schema_version":3,"dimensions":{{"routes":{{"entries":[
{{"path":"/admin","method":"GET","classification":"{classification}","roles":[{roles}]}}]}}}}}}"#
),
)
.unwrap();
};
write(&base, "gated", "\"admin\"");
write(&head, "public", "");
let opts = DiffOptions {
base: base.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "markdown",
output: Some(report.to_str().unwrap()),
acks: &[],
ack_file: None,
ack_dir: None,
allow_missing_base: false,
};
assert_eq!(run_diff(&opts), EXIT_BLOCKED);
let written = std::fs::read_to_string(&report).unwrap();
assert!(written.contains("/admin"), "{written}");
assert!(written.contains(render::COMMENT_MARKER), "{written}");
write(&head, "gated", "\"admin\"");
assert_eq!(run_diff(&opts), 0);
assert_eq!(
std::fs::read_to_string(&report).unwrap(),
"",
"a clean run leaves nothing for the workflow to post"
);
}
#[test]
fn an_acknowledgment_for_a_different_digest_does_not_unblock() {
let base = manifest(&route("/admin", "gated", &["admin"]));
let head = manifest(&route("/admin", "public", &[]));
let e = evaluate(
Some(&base),
&head,
&ack::parse_acks("/ack-posture 0000000000000000"),
);
assert!(e.blocked());
assert!(e.acknowledged.is_none());
}
#[test]
fn the_ack_dir_parses_each_comment_on_its_own() {
let dir = tempfile::tempdir().unwrap();
let base = dir.path().join("base.json");
let head = dir.path().join("head.json");
let write = |path: &std::path::Path, classification: &str, roles: &str| {
std::fs::write(
path,
format!(
r#"{{"schema_version":3,"dimensions":{{"routes":{{"entries":[
{{"path":"/admin","method":"GET","classification":"{classification}","roles":[{roles}]}}]}}}}}}"#
),
)
.unwrap();
};
write(&base, "gated", "\"admin\"");
write(&head, "public", "");
let digest = ack::short(
&evaluate(
Some(&PostureManifest::read(base.to_str().unwrap()).unwrap()),
&PostureManifest::read(head.to_str().unwrap()).unwrap(),
&[],
)
.ack_digest,
);
let acks = dir.path().join("acks");
std::fs::create_dir(&acks).unwrap();
let opts = DiffOptions {
base: base.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "text",
output: None,
acks: &[],
ack_file: None,
ack_dir: Some(acks.to_str().unwrap()),
allow_missing_base: false,
};
std::fs::write(
acks.join("1.md"),
format!(
"```\n{}\n{} {digest}\n```\n",
ack::SOURCE_SEPARATOR,
ack::ACK_PHRASE
),
)
.unwrap();
assert_eq!(run_diff(&opts), EXIT_BLOCKED);
std::fs::write(acks.join("1.md"), "log:\n```\noops\n").unwrap();
std::fs::write(acks.join("2.md"), format!("{} {digest}\n", ack::ACK_PHRASE)).unwrap();
assert_eq!(run_diff(&opts), 0);
}
#[test]
fn a_missing_ack_dir_is_a_usage_error() {
let dir = tempfile::tempdir().unwrap();
let head = dir.path().join("head.json");
std::fs::write(&head, r#"{"schema_version":3}"#).unwrap();
let missing = dir.path().join("no-such-dir");
let code = run_diff(&DiffOptions {
base: head.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "text",
output: None,
acks: &[],
ack_file: None,
ack_dir: Some(missing.to_str().unwrap()),
allow_missing_base: false,
});
assert_eq!(code, EXIT_USAGE);
}
#[test]
fn the_ack_dir_skips_subdirectories() {
let dir = tempfile::tempdir().unwrap();
let base = dir.path().join("base.json");
let head = dir.path().join("head.json");
std::fs::write(
&base,
manifest_json(&route("/admin", "gated", &["admin"]), ""),
)
.unwrap();
std::fs::write(&head, manifest_json(&route("/admin", "public", &[]), "")).unwrap();
let digest = ack::short(
&evaluate(
Some(&PostureManifest::read(base.to_str().unwrap()).unwrap()),
&PostureManifest::read(head.to_str().unwrap()).unwrap(),
&[],
)
.ack_digest,
);
let acks = dir.path().join("acks");
std::fs::create_dir_all(acks.join("sub")).unwrap();
std::fs::write(
acks.join("sub").join("1.md"),
format!("{} {digest}\n", ack::ACK_PHRASE),
)
.unwrap();
let code = run_diff(&DiffOptions {
base: base.to_str().unwrap(),
head: head.to_str().unwrap(),
format: "text",
output: None,
acks: &[],
ack_file: None,
ack_dir: Some(acks.to_str().unwrap()),
allow_missing_base: false,
});
assert_eq!(code, EXIT_BLOCKED);
}
#[test]
fn a_build_change_blocks_until_acknowledged() {
let r = route("/admin", "gated", &["admin"]);
let base = parse(&manifest_json(&r, ""));
let head = parse(&manifest_json(&r, r#""build":{"profile":"release"},"#));
let blocked = evaluate(Some(&base), &head, &[]);
assert!(blocked.blocked());
let json = render(&blocked, "json").unwrap();
assert!(json.contains(r#""kind": "build_changed""#), "{json}");
let comment = format!("{} {}", ack::ACK_PHRASE, ack::short(&blocked.ack_digest));
assert!(!evaluate(Some(&base), &head, &ack::parse_acks(&comment)).blocked());
}
fn scoped(path: &str, classification: &str, scopes: &[&str], policy: bool) -> String {
let scopes = scopes
.iter()
.map(|s| format!("\"{s}\""))
.collect::<Vec<_>>()
.join(",");
format!(
r#"{{"path":"{path}","method":"GET","name":"h","classification":"{classification}",
"roles":[],"scopes":[{scopes}],"policy":{policy},"source":"user","provenance":"provable"}}"#
)
}
fn pasted(e: &Evaluation) -> Vec<Acknowledgment> {
ack::parse_acks(&format!("{} {} for launch", ack::ACK_PHRASE, e.marker))
}
fn admin_base() -> PostureManifest {
manifest(&route("/admin", "gated", &["admin"]))
}
#[test]
fn the_printed_marker_carries_a_ceiling_and_acknowledges() {
let base = admin_base();
let head = manifest(&scoped("/admin", "public", &["mfa"], false));
let blocked = evaluate(Some(&base), &head, &[]);
assert!(blocked.marker.contains("ceiling="), "{}", blocked.marker);
let e = evaluate(Some(&base), &head, &pasted(&blocked));
assert!(!e.blocked());
assert!(e.acknowledged.is_some());
}
#[test]
fn an_acknowledgment_survives_a_later_narrowing() {
let base = admin_base();
let first = manifest(&scoped("/admin", "public", &[], false));
let acks = pasted(&evaluate(Some(&base), &first, &[]));
let narrowed = manifest(&scoped("/admin", "public", &[], true));
let e = evaluate(Some(&base), &narrowed, &acks);
assert!(!e.blocked(), "a policy check only narrows");
assert!(e.acknowledged.is_some());
}
#[test]
fn an_acknowledgment_does_not_survive_a_constraint_being_removed() {
let base = admin_base();
let first = manifest(&scoped("/admin", "public", &["mfa"], false));
let acks = pasted(&evaluate(Some(&base), &first, &[]));
let dropped = manifest(&scoped("/admin", "public", &[], false));
assert!(evaluate(Some(&base), &dropped, &acks).blocked());
}
#[test]
fn a_policy_check_dropped_after_acknowledgment_re_blocks() {
let base = manifest(&scoped("/admin", "gated", &[], true));
let first = manifest(&scoped("/admin", "gated", &[], true));
assert!(evaluate(Some(&base), &first, &[]).findings.is_empty());
let wide = manifest(&scoped("/admin", "public", &[], true));
let acks = pasted(&evaluate(Some(&base), &wide, &[]));
let dropped = manifest(&scoped("/admin", "public", &[], false));
assert!(evaluate(Some(&base), &dropped, &acks).blocked());
}
#[test]
fn a_ceiling_marker_does_not_cover_a_new_widening() {
let base = admin_base();
let first = manifest(&route("/admin", "public", &[]));
let acks = pasted(&evaluate(Some(&base), &first, &[]));
let second = manifest(&format!(
"{},{}",
route("/admin", "public", &[]),
route("/internal", "public", &[])
));
assert!(evaluate(Some(&base), &second, &acks).blocked());
}
#[test]
fn a_hand_widened_ceiling_is_refused() {
let base = admin_base();
let head = manifest(&scoped("/admin", "public", &["mfa"], false));
let strict = evaluate(Some(&base), &head, &[]);
let loose_head = manifest(&scoped("/admin", "public", &[], false));
let loose = evaluate(Some(&base), &loose_head, &[]);
let digest = strict.marker.split_whitespace().next().unwrap().to_owned();
let loose_payload = loose.marker.split("ceiling=").nth(1).unwrap().to_owned();
let forged = ack::parse_acks(&format!(
"{} {digest} ceiling={loose_payload}",
ack::ACK_PHRASE
));
assert!(evaluate(Some(&base), &loose_head, &forged).blocked());
}
#[test]
fn a_bare_digest_still_re_asks_after_a_narrowing() {
let base = admin_base();
let first = manifest(&scoped("/admin", "public", &[], false));
let pasted_bare = format!(
"{} {}",
ack::ACK_PHRASE,
ack::short(&evaluate(Some(&base), &first, &[]).ack_digest)
);
assert!(!evaluate(Some(&base), &first, &ack::parse_acks(&pasted_bare)).blocked());
let narrowed = manifest(&scoped("/admin", "public", &[], true));
assert!(evaluate(Some(&base), &narrowed, &ack::parse_acks(&pasted_bare)).blocked());
}
#[test]
fn a_build_change_is_still_bound_exactly() {
let routes = route("/admin", "gated", &["admin"]);
let base = parse(&manifest_json(&routes, ""));
let head = parse(&manifest_json(
&routes,
r#""build":{"profile":"release","features":["x"],"default_features":true},"#,
));
let blocked = evaluate(Some(&base), &head, &[]);
assert!(blocked.blocked(), "{:?}", blocked.findings);
assert!(
!blocked.marker.contains("ceiling="),
"nothing to order: {}",
blocked.marker
);
assert!(!evaluate(Some(&base), &head, &pasted(&blocked)).blocked());
}
#[test]
fn an_acknowledgment_survives_the_widening_set_shrinking() {
let base = manifest(&format!(
"{},{}",
route("/a", "gated", &["admin"]),
route("/b", "gated", &["admin"])
));
let both = manifest(&format!(
"{},{}",
route("/a", "public", &[]),
route("/b", "public", &[])
));
let acks = pasted(&evaluate(Some(&base), &both, &[]));
let one_back = manifest(&format!(
"{},{}",
route("/a", "gated", &["admin"]),
route("/b", "public", &[])
));
assert!(!evaluate(Some(&base), &one_back, &acks).blocked());
}
#[test]
fn an_acknowledgment_survives_a_narrowing_that_changes_the_finding_kind() {
let base = manifest(&route("/a", "gated", &["admin"]));
let cleared = manifest(&route("/a", "gated", &[]));
let acks = pasted(&evaluate(Some(&base), &cleared, &[]));
let some = manifest(&route("/a", "gated", &["admin", "editor"]));
let e = evaluate(Some(&base), &some, &acks);
assert!(!diff::widening(&e.findings).is_empty());
assert!(!e.blocked());
}
#[test]
fn the_report_names_the_strict_digest_beside_a_ceiling_marker() {
let base = admin_base();
let head = manifest(&route("/admin", "public", &[]));
let e = evaluate(Some(&base), &head, &[]);
let md = render(&e, "markdown").unwrap();
let strict = format!("{} {}", ack::ACK_PHRASE, ack::short(&e.ack_digest));
assert!(md.contains(&strict), "{md}");
let strict_acks = ack::parse_acks(&strict);
assert!(!evaluate(Some(&base), &head, &strict_acks).blocked());
}
}