use regex::Regex;
use serde::Serialize;
use super::ResolvedPolicy;
pub const BENCHMARK_ID: &str = "cgb-v1.0-draft";
pub const CONTROLS_TOTAL: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum CheckStatus {
Pass,
Fail,
Inconclusive,
}
#[derive(Debug, Clone, Serialize)]
pub struct CoverageCheck {
pub control: &'static str,
pub title: &'static str,
pub status: CheckStatus,
pub detail: String,
}
pub(crate) const CREDENTIAL_FIXTURES: &[(&str, &str)] = &[
("private key block", "-----BEGIN RSA PRIVATE KEY-----"),
("cloud access key", "AKIAIOSFODNN7EXAMPLE"),
(
"credential assignment",
"api_key = \"sk-supersecretvalue1234\"",
),
(
"bearer token",
"Authorization: Bearer abcdefghij0123456789xyz",
),
];
pub(crate) const DOMAIN_FIXTURES: &[(&str, &str)] = &[
("IBAN", "DE89 3704 0044 0532 0130 00"),
("payment card", "4111 1111 1111 1111"),
("US SSN", "SSN: 123-45-6789"),
("date of birth", "DOB: 03/14/1975"),
];
const EGRESS_TOOL_HINTS: &[&str] = &["url", "web", "fetch", "http"];
pub fn assess(policy: &ResolvedPolicy) -> Vec<CoverageCheck> {
let patterns: Vec<(String, Regex)> = policy
.redaction
.iter()
.filter_map(|(name, raw)| Regex::new(raw).ok().map(|re| (name.clone(), re)))
.collect();
let matches = |fixture: &str| patterns.iter().any(|(_, re)| re.is_match(fixture));
let mut checks = Vec::new();
let missing: Vec<&str> = CREDENTIAL_FIXTURES
.iter()
.filter(|(_, fixture)| !matches(fixture))
.map(|(class, _)| *class)
.collect();
checks.push(if missing.is_empty() {
CoverageCheck {
control: "CGB-1.1",
title: "credential redaction",
status: CheckStatus::Pass,
detail: format!(
"{}/{} credential fixture classes matched by redaction patterns",
CREDENTIAL_FIXTURES.len(),
CREDENTIAL_FIXTURES.len()
),
}
} else {
CoverageCheck {
control: "CGB-1.1",
title: "credential redaction",
status: CheckStatus::Fail,
detail: format!("unredacted credential classes: {}", missing.join(", ")),
}
});
checks.push(if policy.redaction.is_empty() {
CoverageCheck {
control: "CGB-1.2",
title: "declarative redaction rules",
status: CheckStatus::Fail,
detail: "pack declares no named redaction patterns".to_string(),
}
} else {
CoverageCheck {
control: "CGB-1.2",
title: "declarative redaction rules",
status: CheckStatus::Pass,
detail: format!(
"{} named, versioned patterns (chain: {})",
policy.redaction.len(),
if policy.chain.is_empty() {
"root pack".to_string()
} else {
policy.chain.join(" → ")
}
),
}
});
let domain_hits: Vec<&str> = DOMAIN_FIXTURES
.iter()
.filter(|(_, fixture)| matches(fixture))
.map(|(class, _)| *class)
.collect();
checks.push(if domain_hits.is_empty() {
CoverageCheck {
control: "CGB-1.3",
title: "beyond-secret classification",
status: CheckStatus::Inconclusive,
detail:
"no regulated-identifier patterns declared — acceptable outside regulated workloads"
.to_string(),
}
} else {
CoverageCheck {
control: "CGB-1.3",
title: "beyond-secret classification",
status: CheckStatus::Pass,
detail: format!("regulated classes redacted: {}", domain_hits.join(", ")),
}
});
checks.push(match policy.max_context_tokens {
Some(cap) => CoverageCheck {
control: "CGB-3.2",
title: "context budget cap",
status: CheckStatus::Pass,
detail: format!("max_context_tokens = {cap}"),
},
None => CoverageCheck {
control: "CGB-3.2",
title: "context budget cap",
status: CheckStatus::Inconclusive,
detail: "no cap in pack — verify budget enforcement elsewhere".to_string(),
},
});
checks.push(match policy.audit_retention_days {
Some(days) => CoverageCheck {
control: "CGB-4.3",
title: "audit retention declared",
status: CheckStatus::Pass,
detail: format!("audit_retention_days = {days}"),
},
None => CoverageCheck {
control: "CGB-4.3",
title: "audit retention declared",
status: CheckStatus::Inconclusive,
detail: "no retention expectation in pack".to_string(),
},
});
let denies = policy.deny_tools.len();
checks.push(match (&policy.allow_tools, denies) {
(Some(allow), _) => CoverageCheck {
control: "CGB-5.4",
title: "tool surface scoped",
status: CheckStatus::Pass,
detail: format!(
"allowlist posture: {} tools permitted, rest denied",
allow.len()
),
},
(None, d) if d > 0 => CoverageCheck {
control: "CGB-5.4",
title: "tool surface scoped",
status: CheckStatus::Pass,
detail: format!("denylist posture: {d} denied tool(s)"),
},
_ => CoverageCheck {
control: "CGB-5.4",
title: "tool surface scoped",
status: CheckStatus::Inconclusive,
detail: "pack neither allows nor denies tools — engine defaults apply".to_string(),
},
});
let egress_denied: Vec<&str> = policy
.deny_tools
.iter()
.filter(|t| {
let t = t.to_lowercase();
EGRESS_TOOL_HINTS.iter().any(|h| t.contains(h))
})
.map(String::as_str)
.collect();
let egress_allowed = policy.allow_tools.as_ref().map(|allow| {
allow
.iter()
.filter(|t| {
let t = t.to_lowercase();
EGRESS_TOOL_HINTS.iter().any(|h| t.contains(h))
})
.count()
});
checks.push(if !egress_denied.is_empty() {
CoverageCheck {
control: "CGB-5.5",
title: "egress restricted",
status: CheckStatus::Pass,
detail: format!("egress tools denied: {}", egress_denied.join(", ")),
}
} else if egress_allowed == Some(0) {
CoverageCheck {
control: "CGB-5.5",
title: "egress restricted",
status: CheckStatus::Pass,
detail: "allowlist contains no egress-capable tools".to_string(),
}
} else {
CoverageCheck {
control: "CGB-5.5",
title: "egress restricted",
status: CheckStatus::Inconclusive,
detail: "pack does not restrict egress tools — verify via roles/network policy"
.to_string(),
}
});
checks
}
#[derive(Debug, Serialize)]
pub struct CoverageSummary {
pub pass: usize,
pub fail: usize,
pub inconclusive: usize,
pub controls_covered: usize,
pub controls_total: usize,
}
pub fn summarize(checks: &[CoverageCheck]) -> CoverageSummary {
let mut covered: Vec<&str> = checks.iter().map(|c| c.control).collect();
covered.dedup();
CoverageSummary {
pass: checks
.iter()
.filter(|c| c.status == CheckStatus::Pass)
.count(),
fail: checks
.iter()
.filter(|c| c.status == CheckStatus::Fail)
.count(),
inconclusive: checks
.iter()
.filter(|c| c.status == CheckStatus::Inconclusive)
.count(),
controls_covered: covered.len(),
controls_total: CONTROLS_TOTAL,
}
}
#[cfg(test)]
mod tests {
use super::super::builtin;
use super::*;
fn resolved(name: &str) -> ResolvedPolicy {
let pack = builtin::get(name).expect("built-in exists");
super::super::resolve(&pack).expect("resolves")
}
fn status_of(checks: &[CoverageCheck], control: &str) -> CheckStatus {
checks
.iter()
.find(|c| c.control == control)
.expect("control checked")
.status
}
#[test]
fn baseline_passes_credential_redaction() {
let checks = assess(&resolved("baseline"));
assert_eq!(status_of(&checks, "CGB-1.1"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-1.2"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-1.3"), CheckStatus::Inconclusive);
assert_eq!(status_of(&checks, "CGB-5.4"), CheckStatus::Inconclusive);
}
#[test]
fn finance_eu_demonstrates_domain_classes_and_egress_denial() {
let checks = assess(&resolved("finance-eu"));
assert_eq!(status_of(&checks, "CGB-1.1"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-1.3"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-3.2"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-4.3"), CheckStatus::Pass);
assert_eq!(status_of(&checks, "CGB-5.5"), CheckStatus::Pass);
}
#[test]
fn healthcare_demonstrates_phi_classes() {
let checks = assess(&resolved("healthcare"));
assert_eq!(status_of(&checks, "CGB-1.3"), CheckStatus::Pass);
}
#[test]
fn empty_policy_fails_credential_checks() {
let empty = ResolvedPolicy {
name: "empty".into(),
version: "0.0.1".into(),
description: String::new(),
chain: vec![],
default_read_mode: None,
allow_tools: None,
deny_tools: vec![],
max_context_tokens: None,
audit_retention_days: None,
redaction: std::collections::BTreeMap::new(),
};
let checks = assess(&empty);
assert_eq!(status_of(&checks, "CGB-1.1"), CheckStatus::Fail);
assert_eq!(status_of(&checks, "CGB-1.2"), CheckStatus::Fail);
}
#[test]
fn summary_counts_are_consistent() {
let checks = assess(&resolved("finance-eu"));
let s = summarize(&checks);
assert_eq!(s.pass + s.fail + s.inconclusive, checks.len());
assert_eq!(s.controls_total, CONTROLS_TOTAL);
assert!(s.controls_covered <= checks.len());
}
}