use crate::finding::{Finding, Severity};
use crate::scope::Scope;
use crate::suppression::Inventory;
use headwater_census::shelves::DeclarationError;
use headwater_yaml::{Mapping, Span, Value};
const CHECK: &str = "check:";
const PHASE: &str = "phase:";
pub const PHASES: [&str; 2] = ["census.classification", "runner.coverage_report"];
pub const DISPOSITION: &str = "obligation.disposition.not_one";
pub const MECHANISM: &str = "control.mechanism.unimplemented";
pub const OBSERVATION: &str = "control.observation.invalid";
pub const SCOPE: Scope = Scope::taxonomy();
pub const VERSION: u32 = 1;
pub const EXPORTABLE_AS: crate::scope::ExportTargets = &[];
#[derive(Clone, Debug, Default)]
pub struct Register {
pub obligations: Vec<Obligation>,
pub controls: Vec<Control>,
}
#[derive(Clone, Debug)]
pub struct Obligation {
pub id: String,
pub statement: String,
pub severity: Option<String>,
pub disposition: Option<Stated>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Stated {
Gap {
owner: String,
target: Option<String>,
},
Unverifiable { reasoning: String },
}
#[derive(Clone, Debug)]
pub struct Control {
pub id: String,
pub mechanism: String,
pub discharges: Vec<String>,
pub posture: Option<String>,
pub acts: Option<String>,
pub promotion: Option<Promotion>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Promotion {
Criteria {
window: String,
threshold: String,
sample: String,
},
FinalPosture { reasoning: String },
PermanentlyAdvisory { reasoning: String },
ProducesFacts { reasoning: String },
}
impl Promotion {
pub fn name(&self) -> &'static str {
match self {
Promotion::Criteria { .. } => "with criteria",
Promotion::FinalPosture { .. } => "at a final posture",
Promotion::PermanentlyAdvisory { .. } => "permanently advisory",
Promotion::ProducesFacts { .. } => "producing facts rather than findings",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Mechanism {
Rule(String),
Phase(String),
Unimplemented(String),
External(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Bound {
To(String),
Unnamed,
Several(Vec<String>),
}
impl Register {
pub fn read(root: &Mapping) -> Result<Self, Vec<DeclarationError>> {
let mut errors = Vec::new();
let mut register = Register::default();
if let Some(obligations) = root.get("obligations") {
match &obligations.value {
Value::Map(map) => {
for entry in map {
match read_obligation(&entry.key.value, &entry.value.value, entry.key.span)
{
Ok(obligation) => register.obligations.push(obligation),
Err(error) => errors.push(error),
}
}
}
other => errors.push(DeclarationError {
message: format!(
"`obligations` is {}, and it names obligations",
other.kind_name()
),
span: obligations.span,
}),
}
}
if let Some(controls) = root.get("controls") {
match &controls.value {
Value::Map(map) => {
for entry in map {
match read_control(&entry.key.value, &entry.value.value, entry.key.span) {
Ok(control) => register.controls.push(control),
Err(error) => errors.push(error),
}
}
}
other => errors.push(DeclarationError {
message: format!("`controls` is {}, and it names controls", other.kind_name()),
span: controls.span,
}),
}
}
if errors.is_empty() {
Ok(register)
} else {
Err(errors)
}
}
pub fn bound(&self, rule: &str) -> Bound {
let mut named: Vec<String> = Vec::new();
for control in &self.controls {
let Some(mechanism) = control.mechanism.strip_prefix(CHECK) else {
continue;
};
if mechanism != rule {
continue;
}
for obligation in &control.discharges {
if !named.contains(obligation) {
named.push(obligation.clone());
}
}
}
match named.len() {
0 => Bound::Unnamed,
1 => Bound::To(named.remove(0)),
_ => Bound::Several(named),
}
}
pub fn obligation(&self, id: &str) -> Option<&Obligation> {
self.obligations.iter().find(|entry| entry.id == id)
}
pub fn mechanism(&self, control: &Control) -> Mechanism {
if let Some(rule) = control.mechanism.strip_prefix(CHECK) {
return match crate::RULES.contains(&rule) {
true => Mechanism::Rule(rule.to_string()),
false => Mechanism::Unimplemented(control.mechanism.clone()),
};
}
if let Some(phase) = control.mechanism.strip_prefix(PHASE) {
return match PHASES.contains(&phase) {
true => Mechanism::Phase(phase.to_string()),
false => Mechanism::Unimplemented(control.mechanism.clone()),
};
}
Mechanism::External(control.mechanism.clone())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Disposition {
Verified,
Gap,
Unverifiable,
Undeclared,
}
impl Disposition {
pub fn name(self) -> &'static str {
match self {
Disposition::Verified => "verified",
Disposition::Gap => "gap",
Disposition::Unverifiable => "unverifiable",
Disposition::Undeclared => "with no disposition",
}
}
}
#[derive(Clone, Debug)]
pub struct Disposed {
pub id: String,
pub severity: Option<String>,
pub controls: Vec<String>,
pub unimplemented: Vec<String>,
pub unobserved: Vec<String>,
pub stated: Option<Stated>,
pub escaped: usize,
pub pending: usize,
}
impl Disposed {
pub fn discharged(&self) -> bool {
self.controls.len() > self.unimplemented.len() + self.unobserved.len()
}
fn unresolved_reason(&self) -> String {
let mut parts = Vec::new();
if !self.unimplemented.is_empty() {
parts.push(format!(
"{} {} a mechanism this engine does not implement",
self.unimplemented.join(", "),
verb(self.unimplemented.len(), "names", "name")
));
}
if !self.unobserved.is_empty() {
parts.push(format!(
"{} {} a mechanism outside this engine that no committed observation names",
self.unobserved.join(", "),
verb(self.unobserved.len(), "names", "name")
));
}
parts.join(", and ")
}
fn claim_reason(&self) -> String {
let mut parts = Vec::new();
if !self.unimplemented.is_empty() {
parts.push(format!(
"{} {} to discharge it under a mechanism this engine does not implement",
self.unimplemented.join(", "),
verb(self.unimplemented.len(), "claims", "claim")
));
}
if !self.unobserved.is_empty() {
parts.push(format!(
"{} {} to discharge it under a mechanism outside this engine that no committed \
observation names",
self.unobserved.join(", "),
verb(self.unobserved.len(), "claims", "claim")
));
}
parts.join(", and ")
}
pub fn disposition(&self) -> Disposition {
match (self.discharged(), &self.stated) {
(true, _) => Disposition::Verified,
(false, Some(Stated::Gap { .. })) => Disposition::Gap,
(false, Some(Stated::Unverifiable { .. })) => Disposition::Unverifiable,
(false, None) => Disposition::Undeclared,
}
}
pub fn contradicted(&self) -> bool {
!self.controls.is_empty() && self.stated.is_some()
}
fn claimed_only(&self) -> bool {
!self.controls.is_empty() && !self.discharged()
}
}
#[derive(Clone, Debug)]
pub struct Health {
pub id: String,
pub mechanism: Mechanism,
pub posture: Option<String>,
pub acts: Option<String>,
pub discharges: Vec<String>,
pub promotion: Option<Promotion>,
}
#[derive(Clone, Debug, Default)]
pub struct Projection {
pub obligations: Vec<Disposed>,
pub controls: Vec<Health>,
pub unbound: Vec<(&'static str, Bound)>,
observation_problems: Vec<crate::observation::Problem>,
observation_undeclared: Vec<String>,
observation_duplicate: Vec<String>,
}
impl Projection {
pub fn of(register: &Register, observations: &crate::observation::Observations) -> Self {
let obligations = register
.obligations
.iter()
.map(|obligation| {
let naming: Vec<&Control> = register
.controls
.iter()
.filter(|control| control.discharges.contains(&obligation.id))
.collect();
Disposed {
id: obligation.id.clone(),
severity: obligation.severity.clone(),
controls: naming.iter().map(|control| control.id.clone()).collect(),
unimplemented: naming
.iter()
.filter(|control| {
matches!(register.mechanism(control), Mechanism::Unimplemented(_))
})
.map(|control| control.id.clone())
.collect(),
unobserved: naming
.iter()
.filter(|control| {
matches!(register.mechanism(control), Mechanism::External(_))
&& !observations.observed(&control.id)
})
.map(|control| control.id.clone())
.collect(),
stated: obligation.disposition.clone(),
escaped: 0,
pending: 0,
}
})
.collect();
let controls = register
.controls
.iter()
.map(|control| Health {
id: control.id.clone(),
mechanism: register.mechanism(control),
posture: control.posture.clone(),
acts: control.acts.clone(),
discharges: control.discharges.clone(),
promotion: control.promotion.clone(),
})
.collect();
let unbound = crate::RULES
.iter()
.filter_map(|rule| match register.bound(rule) {
Bound::To(_) => None,
other => Some((*rule, other)),
})
.collect();
let observation_undeclared = observations
.entries()
.iter()
.filter_map(|entry| match entry {
crate::observation::Observation::Control { control, .. } => Some(control),
crate::observation::Observation::Verification { .. } => None,
})
.filter(|control| {
!register
.controls
.iter()
.any(|declared| &declared.id == *control)
})
.cloned()
.collect();
let observation_duplicate = crate::observation::duplicate_ids(observations.entries());
Projection {
obligations,
controls,
unbound,
observation_problems: observations.problems().to_vec(),
observation_undeclared,
observation_duplicate,
}
}
pub fn pending_from(&mut self, register: &Register, ledger: &crate::adoption::Ledger) {
for (rule, count) in ledger.by_rule() {
let Bound::To(obligation) = register.bound(rule) else {
continue;
};
if let Some(disposed) = self.obligations.iter_mut().find(|d| d.id == obligation) {
disposed.pending += count;
}
}
}
pub fn escaped_from(&mut self, register: &Register, inventory: &Inventory) {
for (rule, count) in inventory.by_rule() {
let Bound::To(obligation) = register.bound(rule) else {
continue;
};
if let Some(disposed) = self.obligations.iter_mut().find(|d| d.id == obligation) {
disposed.escaped += count;
}
}
}
pub fn findings(&self, source: &str) -> Vec<Finding> {
let mut findings = Vec::new();
for obligation in &self.obligations {
let id = &obligation.id;
let nothing = match obligation.claimed_only() {
true => format!(
"{}, so nothing discharges it",
obligation.unresolved_reason()
),
false => "no control discharges it".to_string(),
};
let message = match obligation.disposition() {
Disposition::Undeclared => Some(format!(
"obligation {id} carries no disposition: {nothing}, and it states neither a \
gap nor an acceptance"
)),
_ if obligation.contradicted() => Some(match obligation.claimed_only() {
true => format!(
"obligation {id} carries two dispositions: {} claims to discharge it, and \
it also states one for itself",
obligation.controls.join(", ")
),
false => format!(
"obligation {id} carries two dispositions: {} discharges it, and it also \
states one for itself",
obligation.controls.join(", ")
),
}),
_ => None,
};
let Some(message) = message else { continue };
findings.push(Finding {
rule: DISPOSITION,
severity: Severity::Warn,
obligation: None,
path: source.to_string(),
line: 0,
column: 0,
message,
remediation: format!(
"{}, or state `disposition: {{gap: {{owner: …}}}}` or `disposition: \
{{unverifiable: {{reasoning: …}}}}` on it, and exactly one of the three",
match obligation.claimed_only() {
true => {
let mut clauses = Vec::new();
if !obligation.unimplemented.is_empty() {
clauses.push(format!(
"name a mechanism this engine implements on {}",
obligation.unimplemented.join(", ")
));
}
if !obligation.unobserved.is_empty() {
clauses.push(format!(
"add an entry for {} to `.headwater/observations.yml` \
naming the commit it ran against, e.g. `{}: {{commit: \
<sha>}}`",
obligation.unobserved.join(", "),
obligation.unobserved[0]
));
}
clauses.join(", or ")
}
false => "give the obligation a control that discharges it".to_string(),
}
),
patch: None,
});
}
for control in &self.controls {
let Mechanism::Unimplemented(mechanism) = &control.mechanism else {
continue;
};
findings.push(Finding {
rule: MECHANISM,
severity: Severity::Warn,
obligation: None,
path: source.to_string(),
line: 0,
column: 0,
message: format!(
"control {} names the mechanism {mechanism}, which this engine does not \
implement, so nothing discharges {}",
control.id,
control.discharges.join(", ")
),
remediation: "name a rule this engine carries, or a phase of it, or a mechanism \
outside it under a prefix this engine does not read"
.to_string(),
patch: None,
});
}
for problem in &self.observation_problems {
let (message, remediation) = match problem {
crate::observation::Problem::File(message) => (
message.clone(),
"read `.headwater/observations.yml` by hand and repair it, or delete it: an \
absent file reads as no observation, and a malformed one should read the \
same way rather than silently"
.to_string(),
),
crate::observation::Problem::Entry { id, reason, .. } => (
format!("`.headwater/observations.yml` names `{id}`, and {reason}"),
"remove the entry, or correct its shape".to_string(),
),
};
findings.push(Finding {
rule: OBSERVATION,
severity: Severity::Warn,
obligation: None,
path: source.to_string(),
line: 0,
column: 0,
message,
remediation,
patch: None,
});
}
for control_id in &self.observation_undeclared {
findings.push(Finding {
rule: OBSERVATION,
severity: Severity::Warn,
obligation: None,
path: source.to_string(),
line: 0,
column: 0,
message: format!(
"`.headwater/observations.yml` names `{control_id}`, which no control in \
this taxonomy declares"
),
remediation: "remove the entry, or correct the control id it names".to_string(),
patch: None,
});
}
for control_id in &self.observation_duplicate {
findings.push(Finding {
rule: OBSERVATION,
severity: Severity::Warn,
obligation: None,
path: source.to_string(),
line: 0,
column: 0,
message: format!(
"`.headwater/observations.yml` names `{control_id}` more than once"
),
remediation: "remove every entry for the control but one".to_string(),
patch: None,
});
}
findings
}
pub fn at(&self, disposition: Disposition) -> impl Iterator<Item = &Disposed> {
self.obligations
.iter()
.filter(move |o| o.disposition() == disposition)
}
pub fn by_severity(&self) -> Vec<(&str, usize, usize)> {
let mut rows: Vec<(&str, usize, usize)> = ["high", "medium", "low", "-"]
.into_iter()
.map(|severity| (severity, 0, 0))
.collect();
for obligation in &self.obligations {
let severity = obligation.severity.as_deref().unwrap_or("-");
let row = match rows.iter_mut().find(|(known, _, _)| *known == severity) {
Some(row) => row,
None => {
rows.push((severity, 0, 0));
rows.last_mut().expect("just pushed")
}
};
row.1 += 1;
if obligation.disposition() == Disposition::Verified {
row.2 += 1;
}
}
rows.retain(|(_, count, _)| *count > 0);
rows
}
pub fn render(&self) -> String {
use std::fmt::Write;
let mut out = String::new();
if self.obligations.is_empty() && self.controls.is_empty() {
return out;
}
out.push_str("register\n");
let counts: Vec<String> = [
Disposition::Verified,
Disposition::Gap,
Disposition::Unverifiable,
Disposition::Undeclared,
]
.into_iter()
.map(|disposition| format!("{} {}", self.at(disposition).count(), disposition.name()))
.collect();
let _ = writeln!(
out,
" {} obligations: {}",
self.obligations.len(),
counts.join(", ")
);
for (severity, count, verified) in self.by_severity() {
let _ = writeln!(out, " {count:5} {severity}, {verified} verified");
}
for obligation in self.obligations.iter().filter(|o| {
o.disposition() != Disposition::Verified
|| o.contradicted()
|| o.escaped > 0
|| o.pending > 0
}) {
let _ = write!(
out,
" {} {}",
obligation.id,
obligation.disposition().name()
);
let _ = match (obligation.disposition(), &obligation.stated) {
(Disposition::Gap, Some(Stated::Gap { owner, target })) => match target {
Some(target) => write!(out, ", owner {owner}, target {target}"),
None => write!(out, ", owner {owner}, and no target"),
},
(Disposition::Unverifiable, Some(Stated::Unverifiable { reasoning })) => {
write!(out, ", {reasoning}")
}
_ => Ok(()),
};
if obligation.claimed_only() {
let _ = write!(out, ", and {}", obligation.claim_reason());
}
if obligation.contradicted() {
out.push_str(match (obligation.claimed_only(), &obligation.stated) {
(true, _) => ", which is two dispositions",
(false, Some(Stated::Gap { .. })) => {
", and it states a gap as well, which is two dispositions"
}
(false, _) => {
", and it states an acceptance as well, which is two dispositions"
}
});
}
let _ = match obligation.escaped {
0 => Ok(()),
1 => write!(out, ", 1 finding escaped under it"),
escaped => write!(out, ", {escaped} findings escaped under it"),
};
let _ = match obligation.pending {
0 => Ok(()),
1 => write!(out, ", 1 finding is migration-pending under it"),
pending => write!(out, ", {pending} findings are migration-pending under it"),
};
out.push('\n');
}
let _ = writeln!(out, " {} controls", self.controls.len());
for (label, mut values) in [
(
"posture",
self.tally(self.controls.iter().map(|c| c.posture.as_deref())),
),
(
"acts",
self.tally(self.controls.iter().map(|c| c.acts.as_deref())),
),
] {
values.sort_unstable();
for (value, count) in values {
let _ = writeln!(out, " {count:5} {label} {value}");
}
}
for (mechanism, count) in self.mechanisms() {
let _ = writeln!(out, " {count:5} {mechanism}");
}
let bare = self
.controls
.iter()
.filter(|control| control.promotion.is_none())
.count();
if bare > 0 {
let _ = writeln!(
out,
" {bare:5} with no promotion record, so nothing states what would promote them"
);
}
for name in [
"with criteria",
"at a final posture",
"permanently advisory",
"producing facts rather than findings",
] {
let count = self
.controls
.iter()
.filter(|control| control.promotion.as_ref().is_some_and(|p| p.name() == name))
.count();
if count > 0 {
let _ = writeln!(out, " {count:5} {name}");
}
}
let escaped: usize = self.obligations.iter().map(|o| o.escaped).sum();
let pending: usize = self.obligations.iter().map(|o| o.pending).sum();
let _ = writeln!(
out,
" escaped findings, in the precedence spec 4 fixes: no waiver reaches a check \
finding, {pending} migration-pending, {escaped} suppressed"
);
match self.unbound.is_empty() {
true => out.push_str(" every rule this engine carries reaches one obligation\n"),
false => {
for (rule, bound) in &self.unbound {
let _ = match bound {
Bound::Unnamed => {
writeln!(out, " {rule} reaches no obligation, so it names none")
}
Bound::Several(obligations) => writeln!(
out,
" {rule} reaches {}, and a finding names one obligation, so it \
names none",
obligations.join(", ")
),
Bound::To(_) => Ok(()),
};
}
}
}
out
}
fn mechanisms(&self) -> Vec<(&'static str, usize)> {
let mut rows = [
("name a rule this engine carries", 0),
("name a phase this engine runs", 0),
("name a mechanism this engine does not implement", 0),
("name a mechanism outside this engine", 0),
];
for control in &self.controls {
rows[match control.mechanism {
Mechanism::Rule(_) => 0,
Mechanism::Phase(_) => 1,
Mechanism::Unimplemented(_) => 2,
Mechanism::External(_) => 3,
}]
.1 += 1;
}
rows.into_iter().filter(|(_, count)| *count > 0).collect()
}
fn tally<'a>(&self, values: impl Iterator<Item = Option<&'a str>>) -> Vec<(&'a str, usize)> {
let mut counts: Vec<(&str, usize)> = Vec::new();
for value in values {
let value = value.unwrap_or("-");
match counts.iter_mut().find(|(known, _)| *known == value) {
Some((_, count)) => *count += 1,
None => counts.push((value, 1)),
}
}
counts
}
}
fn verb(count: usize, one: &'static str, many: &'static str) -> &'static str {
match count {
1 => one,
_ => many,
}
}
fn read_obligation(id: &str, value: &Value, span: Span) -> Result<Obligation, DeclarationError> {
let map = value.as_map().ok_or_else(|| DeclarationError {
message: format!(
"obligation `{id}` is {}, and an obligation is a mapping",
value.kind_name()
),
span,
})?;
let statement = scalar(map, "statement").ok_or_else(|| DeclarationError {
message: format!("obligation `{id}` states no invariant, so nothing can serve it"),
span,
})?;
Ok(Obligation {
id: id.to_string(),
statement,
severity: scalar(map, "severity"),
disposition: read_disposition(id, map)?,
span,
})
}
fn read_disposition(id: &str, map: &Mapping) -> Result<Option<Stated>, DeclarationError> {
let Some(node) = map.get("disposition") else {
return Ok(None);
};
let Some(stated) = node.value.as_map() else {
return Err(DeclarationError {
message: format!(
"the disposition of obligation `{id}` is {}, and it names one of `gap` and \
`unverifiable`",
node.value.kind_name()
),
span: node.span,
});
};
if let Some(gap) = stated.get("gap").and_then(|node| node.value.as_map()) {
let owner = scalar(gap, "owner").ok_or_else(|| DeclarationError {
message: format!(
"the gap of obligation `{id}` names no owner, and spec 4 tracks a gap with one"
),
span: node.span,
})?;
return Ok(Some(Stated::Gap {
owner,
target: scalar(gap, "target"),
}));
}
if let Some(accepted) = stated
.get("unverifiable")
.and_then(|node| node.value.as_map())
{
let reasoning = scalar(accepted, "reasoning").ok_or_else(|| DeclarationError {
message: format!(
"obligation `{id}` is accepted as unverifiable and records no reasoning, which \
is the half of that disposition spec 4 asks for"
),
span: node.span,
})?;
return Ok(Some(Stated::Unverifiable { reasoning }));
}
Err(DeclarationError {
message: match stated.get("verified").is_some() {
true => format!(
"obligation `{id}` declares itself verified, and no obligation may. Verified \
follows from a control that discharges it, and a second place to write the \
binding is what the register exists to prevent"
),
false => format!(
"the disposition of obligation `{id}` names neither `gap` nor `unverifiable`"
),
},
span: node.span,
})
}
fn read_promotion(id: &str, map: &Mapping) -> Result<Option<Promotion>, DeclarationError> {
let Some(node) = map.get("promotion") else {
return Ok(None);
};
let Some(record) = node.value.as_map() else {
return Err(DeclarationError {
message: format!(
"the promotion record of control `{id}` is {}, and it names one of `criteria`, \
`final_posture`, `permanently_advisory` and `produces_facts`",
node.value.kind_name()
),
span: node.span,
});
};
if let Some(criteria) = record.get("criteria").and_then(|node| node.value.as_map()) {
let missing = |member: &str| DeclarationError {
message: format!(
"the promotion criteria of control `{id}` state no {member}, and spec 4 names it"
),
span: node.span,
};
return Ok(Some(Promotion::Criteria {
window: scalar(criteria, "window").ok_or_else(|| missing("observation window"))?,
threshold: scalar(criteria, "threshold").ok_or_else(|| missing("threshold"))?,
sample: scalar(criteria, "sample").ok_or_else(|| missing("adjudicated sample"))?,
}));
}
for (key, build) in [
(
"final_posture",
(|reasoning| Promotion::FinalPosture { reasoning }) as fn(String) -> Promotion,
),
("permanently_advisory", |reasoning| {
Promotion::PermanentlyAdvisory { reasoning }
}),
("produces_facts", |reasoning| Promotion::ProducesFacts {
reasoning,
}),
] {
let Some(exemption) = record.get(key).and_then(|node| node.value.as_map()) else {
continue;
};
let reasoning = scalar(exemption, "reasoning").ok_or_else(|| DeclarationError {
message: format!(
"control `{id}` is off the promotion path under `{key}` and records no \
reasoning, which leaves the assertion of taste that spec 4 refuses"
),
span: node.span,
})?;
return Ok(Some(build(reasoning)));
}
Err(DeclarationError {
message: format!(
"the promotion record of control `{id}` names none of `criteria`, `final_posture`, \
`permanently_advisory` and `produces_facts`"
),
span: node.span,
})
}
fn read_control(id: &str, value: &Value, span: Span) -> Result<Control, DeclarationError> {
let map = value.as_map().ok_or_else(|| DeclarationError {
message: format!(
"control `{id}` is {}, and a control is a mapping",
value.kind_name()
),
span,
})?;
let mechanism = scalar(map, "mechanism").ok_or_else(|| DeclarationError {
message: format!("control `{id}` names no mechanism, so nothing discharges through it"),
span,
})?;
let discharges = map
.get("discharges")
.and_then(|node| node.value.as_seq())
.map(|items| {
items
.iter()
.filter_map(|item| item.value.as_scalar().map(|scalar| scalar.text.clone()))
.collect()
})
.unwrap_or_default();
Ok(Control {
id: id.to_string(),
mechanism,
discharges,
posture: scalar(map, "posture"),
acts: scalar(map, "acts"),
promotion: read_promotion(id, map)?,
span,
})
}
fn scalar(map: &Mapping, key: &str) -> Option<String> {
map.get(key)
.and_then(|node| node.value.as_scalar())
.map(|scalar| scalar.text.clone())
}
#[cfg(test)]
mod tests {
use super::*;
fn register(source: &str) -> Register {
let root = headwater_yaml::load(source).expect("the source loads");
Register::read(root.value.as_map().expect("a mapping")).expect("the register reads")
}
fn refusal(source: &str) -> String {
let root = headwater_yaml::load(source).expect("the source loads");
Register::read(root.value.as_map().expect("a mapping"))
.expect_err("the register is refused")
.iter()
.map(|error| error.message.clone())
.collect::<Vec<String>>()
.join("\n")
}
const ONE: &str = "\
obligations:
OB-REL-1:
statement: A relation that requires reciprocity is declared at both ends
severity: medium
controls:
CT-REL-1:
mechanism: check:relation.reciprocity.missing
discharges: [OB-REL-1]
";
#[test]
fn a_rule_reaches_its_obligation_through_the_control_that_names_it() {
let register = register(ONE);
assert_eq!(
register.bound("relation.reciprocity.missing"),
Bound::To("OB-REL-1".to_string())
);
assert_eq!(
register.obligation("OB-REL-1").map(|o| o.severity.clone()),
Some(Some("medium".to_string()))
);
}
#[test]
fn a_rule_that_no_control_names_is_unnamed() {
assert_eq!(
register(ONE).bound("shelf.placement_is_primary"),
Bound::Unnamed
);
}
#[test]
fn a_mechanism_that_is_not_a_check_binds_nothing() {
let register = register(
"controls:\n CT-1:\n mechanism: scheduled:staleness-sweep\n discharges: [OB-3]\n",
);
assert_eq!(register.bound("staleness-sweep"), Bound::Unnamed);
}
#[test]
fn two_obligations_on_one_rule_bind_neither() {
let register = register(
"controls:\n CT-1:\n mechanism: check:r\n discharges: [OB-1]\n \
CT-2:\n mechanism: check:r\n discharges: [OB-2]\n",
);
assert_eq!(
register.bound("r"),
Bound::Several(vec!["OB-1".to_string(), "OB-2".to_string()])
);
}
#[test]
fn an_obligation_states_a_gap_or_an_acceptance_and_never_verified() {
let register = register(
"obligations:\n \
OB-1:\n statement: s\n disposition: {gap: {owner: o, target: t}}\n \
OB-2:\n statement: s\n disposition: {unverifiable: {reasoning: r}}\n",
);
assert_eq!(
register
.obligation("OB-1")
.and_then(|o| o.disposition.clone()),
Some(Stated::Gap {
owner: "o".to_string(),
target: Some("t".to_string()),
})
);
assert_eq!(
register
.obligation("OB-2")
.and_then(|o| o.disposition.clone()),
Some(Stated::Unverifiable {
reasoning: "r".to_string(),
})
);
let refused =
refusal("obligations:\n OB-1:\n statement: s\n disposition: {verified: {}}\n");
assert!(refused.contains("declares itself verified"), "{refused}");
}
#[test]
fn each_disposition_carries_the_half_spec_4_asks_for() {
assert!(
refusal("obligations:\n OB-1:\n statement: s\n disposition: {gap: {}}\n")
.contains("names no owner")
);
assert!(refusal(
"obligations:\n OB-1:\n statement: s\n disposition: {unverifiable: {}}\n"
)
.contains("records no reasoning"));
}
#[test]
fn a_phase_discharges_an_obligation_and_binds_no_finding() {
let register = register(
"obligations:\n OB-1:\n statement: s\n\
controls:\n CT-1:\n mechanism: phase:census.classification\n \
discharges: [OB-1]\n",
);
assert_eq!(
register.mechanism(®ister.controls[0]),
Mechanism::Phase("census.classification".to_string())
);
assert_eq!(register.bound("census.classification"), Bound::Unnamed);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
assert_eq!(
projection.obligations[0].disposition(),
Disposition::Verified
);
assert!(projection.findings("t.yml").is_empty());
}
#[test]
fn the_register_reports_what_spec_4_calls_a_finding_about_itself() {
let register = register(
"obligations:\n \
OB-1:\n statement: s\n \
OB-2:\n statement: s\n disposition: {gap: {owner: o}}\n \
OB-3:\n statement: s\n\
controls:\n \
CT-1:\n mechanism: check:coverage.document_unchecked\n discharges: [OB-2]\n \
CT-2:\n mechanism: check:no.such.rule\n discharges: [OB-3]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
let findings = projection.findings("t.yml");
let messages: Vec<&str> = findings.iter().map(|f| f.message.as_str()).collect();
assert_eq!(findings.len(), 4, "{messages:?}");
assert!(messages[0].contains("OB-1 carries no disposition"));
assert!(messages[1].contains("OB-2 carries two dispositions"));
assert!(messages[2].contains("OB-3 carries no disposition"));
assert!(messages[3].contains("does not implement"));
assert!(findings.iter().all(|f| f.path == "t.yml"));
}
#[test]
fn a_control_this_engine_cannot_run_verifies_nothing() {
let register = register(
"obligations:\n OB-1:\n statement: s\n\
controls:\n CT-1:\n mechanism: check:no.such.rule\n discharges: [OB-1]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
let disposed = &projection.obligations[0];
assert_eq!(disposed.controls, vec!["CT-1".to_string()]);
assert_eq!(disposed.unimplemented, vec!["CT-1".to_string()]);
assert!(!disposed.discharged());
assert_eq!(disposed.disposition(), Disposition::Undeclared);
let messages: Vec<String> = projection
.findings("t.yml")
.into_iter()
.map(|finding| finding.message)
.collect();
assert!(
messages[0].contains(
"OB-1 carries no disposition: CT-1 names a mechanism this engine does not \
implement, so nothing discharges it"
),
"{messages:?}"
);
assert!(
messages[1].contains("control CT-1 names the mechanism"),
"{messages:?}"
);
assert!(
projection.render().contains("1 obligations: 0 verified"),
"{}",
projection.render()
);
}
#[test]
fn one_control_this_engine_runs_is_enough() {
let register = register(
"obligations:\n OB-1:\n statement: s\n\
controls:\n \
CT-1:\n mechanism: check:no.such.rule\n discharges: [OB-1]\n \
CT-2:\n mechanism: check:coverage.document_unchecked\n discharges: [OB-1]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
assert_eq!(
projection.obligations[0].disposition(),
Disposition::Verified
);
assert_eq!(projection.findings("t.yml").len(), 1, "the mechanism only");
}
#[test]
fn a_stated_gap_stands_where_the_control_that_claims_it_cannot_run() {
let register = register(
"obligations:\n OB-1:\n statement: s\n disposition: {gap: {owner: o}}\n\
controls:\n CT-1:\n mechanism: check:no.such.rule\n discharges: [OB-1]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
assert_eq!(projection.obligations[0].disposition(), Disposition::Gap);
assert!(projection.obligations[0].contradicted());
let rendered = projection.render();
assert!(
rendered.contains(
"OB-1 gap, owner o, and no target, and CT-1 claims to discharge it under a \
mechanism this engine does not implement, which is two dispositions"
),
"{rendered}"
);
}
#[test]
fn a_mechanism_outside_this_engine_is_not_a_mechanism_finding() {
let register = register(
"obligations:\n OB-1:\n statement: s\n disposition: \
{gap: {owner: o}}\n\
controls:\n CT-1:\n mechanism: scheduled:staleness-sweep\n \
discharges: [OB-1]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
let findings = projection.findings("t.yml");
assert!(findings.iter().all(|f| f.rule != MECHANISM), "{findings:?}");
}
#[test]
fn an_external_mechanism_with_no_observation_is_not_verified() {
let register = register(
"obligations:\n OB-EXT-BARE:\n statement: s\n \
OB-EXT-1:\n statement: s\n\
controls:\n CT-EXT-1:\n mechanism: ci:nightly-suite\n \
discharges: [OB-EXT-1]\n",
);
let observed = Projection::of(®ister, &crate::observation::Observations::empty());
let ob_ext_1 = observed
.obligations
.iter()
.find(|o| o.id == "OB-EXT-1")
.expect("OB-EXT-1 is in the projection");
assert_ne!(ob_ext_1.disposition(), Disposition::Verified);
assert_eq!(ob_ext_1.unobserved, vec!["CT-EXT-1".to_string()]);
assert!(
observed.render().contains("OB-EXT-1"),
"{}",
observed.render()
);
let seen =
crate::observation::Observations::of(vec![crate::observation::Observation::Control {
control: "CT-EXT-1".to_string(),
commit: "788885a9".to_string(),
}]);
let projection = Projection::of(®ister, &seen);
let ob_ext_1 = projection
.obligations
.iter()
.find(|o| o.id == "OB-EXT-1")
.expect("OB-EXT-1 is in the projection");
assert_eq!(ob_ext_1.disposition(), Disposition::Verified);
}
#[test]
fn the_disposition_remediation_names_an_unobserved_control() {
let register = register(
"obligations:\n OB-EXT-1:\n statement: s\n\
controls:\n CT-EXT-1:\n mechanism: ci:nightly-suite\n \
discharges: [OB-EXT-1]\n",
);
let projection = Projection::of(®ister, &crate::observation::Observations::empty());
let findings = projection.findings("t.yml");
let disposition = findings
.iter()
.find(|f| f.rule == DISPOSITION)
.expect("the obligation carries no disposition, so DISPOSITION fires");
assert!(
disposition.remediation.contains("CT-EXT-1"),
"{}",
disposition.remediation
);
assert!(
disposition
.remediation
.contains(".headwater/observations.yml"),
"an unobserved control's remediation should name the file an author writes to \
fix it: {}",
disposition.remediation
);
}
#[test]
fn an_unreadable_snapshot_is_a_finding_and_not_a_silent_empty_read() {
let register = register(
"obligations:\n OB-EXT-1:\n statement: s\n\
controls:\n CT-EXT-1:\n mechanism: ci:nightly-suite\n \
discharges: [OB-EXT-1]\n",
);
let malformed = crate::observation::Observations::malformed_for_test(
"the snapshot did not parse".to_string(),
);
let projection = Projection::of(®ister, &malformed);
let disposed = &projection.obligations[0];
assert_ne!(disposed.disposition(), Disposition::Verified);
let findings = projection.findings("t.yml");
let observation = findings
.iter()
.find(|f| f.rule == OBSERVATION)
.expect("an unreadable snapshot is a finding, not silence");
assert!(
observation.message.contains("the snapshot did not parse"),
"{}",
observation.message
);
}
#[test]
fn an_entry_naming_an_undeclared_control_is_a_finding() {
let register = register(
"obligations:\n OB-EXT-1:\n statement: s\n\
controls:\n CT-EXT-1:\n mechanism: ci:nightly-suite\n \
discharges: [OB-EXT-1]\n",
);
let observations = crate::observation::Observations::of(vec![
crate::observation::Observation::Control {
control: "CT-EXT-1".to_string(),
commit: "788885a9".to_string(),
},
crate::observation::Observation::Control {
control: "CT-NO-SUCH-CONTROL".to_string(),
commit: "788885a9".to_string(),
},
]);
let projection = Projection::of(®ister, &observations);
let findings = projection.findings("t.yml");
let observation = findings
.iter()
.find(|f| f.rule == OBSERVATION)
.expect("an entry naming an undeclared control is a finding");
assert!(
observation.message.contains("CT-NO-SUCH-CONTROL"),
"{}",
observation.message
);
let disposed = &projection.obligations[0];
assert_eq!(disposed.disposition(), Disposition::Verified);
}
#[test]
fn a_control_named_twice_by_the_snapshot_is_a_finding() {
let register = register(
"obligations:\n OB-EXT-1:\n statement: s\n\
controls:\n CT-EXT-1:\n mechanism: ci:nightly-suite\n \
discharges: [OB-EXT-1]\n",
);
let observations = crate::observation::Observations::of(vec![
crate::observation::Observation::Control {
control: "CT-EXT-1".to_string(),
commit: "aaa".to_string(),
},
crate::observation::Observation::Control {
control: "CT-EXT-1".to_string(),
commit: "bbb".to_string(),
},
]);
let projection = Projection::of(®ister, &observations);
let findings = projection.findings("t.yml");
let observation = findings
.iter()
.find(|f| f.rule == OBSERVATION)
.expect("a control named twice is a finding");
assert!(
observation.message.contains("CT-EXT-1"),
"{}",
observation.message
);
let disposed = &projection.obligations[0];
assert_eq!(disposed.disposition(), Disposition::Verified);
}
#[test]
fn a_promotion_record_holds_one_case_and_states_its_reason() {
let register = register(
"controls:\n \
CT-1:\n mechanism: check:r\n promotion: {criteria: \
{window: w, threshold: t, sample: s}}\n \
CT-2:\n mechanism: check:r\n promotion: {permanently_advisory: \
{reasoning: r}}\n",
);
assert_eq!(
register.controls[0].promotion,
Some(Promotion::Criteria {
window: "w".to_string(),
threshold: "t".to_string(),
sample: "s".to_string(),
})
);
assert_eq!(
register.controls[1].promotion,
Some(Promotion::PermanentlyAdvisory {
reasoning: "r".to_string(),
})
);
assert!(refusal(
"controls:\n CT-1:\n mechanism: check:r\n promotion: {final_posture: {}}\n"
)
.contains("records no reasoning"));
assert!(refusal(
"controls:\n CT-1:\n mechanism: check:r\n promotion: {criteria: {window: w}}\n"
)
.contains("state no threshold"));
}
#[test]
fn what_escaped_is_counted_against_the_obligation_and_moves_no_disposition() {
let register = register(
"obligations:\n OB-1:\n statement: s\n\
controls:\n CT-1:\n mechanism: check:coverage.document_unchecked\n \
discharges: [OB-1]\n",
);
let mut projection = Projection::of(®ister, &crate::observation::Observations::empty());
projection.escaped_from(®ister, &Inventory::default());
assert_eq!(projection.obligations[0].escaped, 0);
assert_eq!(
projection.obligations[0].disposition(),
Disposition::Verified
);
}
}