Skip to main content

krabka_guard/
report.rs

1//! The lines this tool prints for a broker's answer, and the exit code that
2//! answer becomes.
3//!
4//! One broker error code maps to one exit code here, so every subcommand hands
5//! a runbook the same number for the same situation. A registry that the tool
6//! checked locally reports what the check proved, entry by entry, and reserves
7//! the signature exit code for a signature that failed rather than one it could
8//! not check.
9
10use {
11    super::{
12        EXIT_BAD_SIGNATURE, EXIT_MISMATCH, EXIT_NO_APPROVAL, EXIT_REFUSED,
13        cli::{action_name, pattern_name},
14        verify::{CheckedEntry, VerifyOutcome},
15    },
16    krabka_protocol::krabka::{break_glass as bg, freeze as api},
17};
18
19/// The exit code that one broker error code becomes.
20///
21/// Three codes get their own number because a runbook acts on them
22/// differently. An action that needs an approval sends the operator for a
23/// second person. A signature failure sends them to their key material. Every
24/// other refusal is a refusal.
25#[must_use]
26pub fn exit_for_code(code: i16) -> i32 {
27    use crate::broker_codes as codes;
28
29    match code {
30        0 => 0,
31        codes::BREAK_GLASS_APPROVAL_REQUIRED => EXIT_NO_APPROVAL,
32        codes::OPERATOR_SIGNATURE_INVALID | codes::OPERATOR_SIGNATURE_REQUIRED => {
33            EXIT_BAD_SIGNATURE
34        }
35        _ => EXIT_REFUSED,
36    }
37}
38
39/// Describe an error code.
40///
41/// `krabka-broker` has no code-to-name table, so this prints the number and
42/// names the krabka-private codes that no Kafka reference lists.
43#[must_use]
44pub fn code_name(code: i16) -> String {
45    use crate::broker_codes as codes;
46
47    let note = match code {
48        codes::BREAK_GLASS_APPROVAL_REQUIRED => {
49            Some("no approved break-glass proposal covers this")
50        }
51        codes::BREAK_GLASS_DUPLICATE_APPROVER => Some("this principal already approved"),
52        codes::BREAK_GLASS_NOT_AN_APPROVER => Some("this principal is not a configured approver"),
53        codes::OPERATOR_SIGNATURE_INVALID => Some("the operator signature did not verify"),
54        codes::OPERATOR_SIGNATURE_REQUIRED => Some("this action needs an operator signature"),
55        codes::FREEZE_SCOPE_INVALID => Some("the scope is empty or reaches an internal topic"),
56        codes::FREEZE_LIMIT_EXCEEDED => Some("the registry is at freeze.max_entries"),
57        _ => None,
58    };
59    note.map_or_else(
60        || format!("error {code}"),
61        |note| format!("error {code} ({note})"),
62    )
63}
64
65/// One error code and its message, as a line.
66pub fn described_error(code: i16, message: Option<&str>) -> String {
67    format!(
68        "{}{}",
69        code_name(code),
70        message.map_or_else(String::new, |m| format!(": {m}"))
71    )
72}
73
74/// Print a refusal and turn it into an exit code.
75pub fn report_error(code: i16, message: Option<&str>) -> i32 {
76    eprintln!("{}", described_error(code, message));
77    exit_for_code(code)
78}
79
80/// The outcome of one freeze or thaw.
81pub fn report_set_freeze(response: &api::SetTopicFreezeResponse, frozen: bool) -> i32 {
82    if response.error_code != 0 {
83        return report_error(response.error_code, response.error_message.as_deref());
84    }
85    println!("{}\tok", if frozen { "frozen" } else { "thawed" });
86    0
87}
88
89/// One line per registry entry.
90pub fn print_freeze(freeze: &api::DescribedTopicFreeze, checked: Option<&CheckedEntry>) {
91    let proof = match checked.map(|entry| entry.unproved) {
92        None => String::new(),
93        Some(None) => format!("\tverified by {}", freeze.key_id),
94        Some(Some(unproved)) => format!("\t{}", unproved.reason()),
95    };
96    println!(
97        "{}:{}\tset by {} at {}\t{}{proof}",
98        pattern_name(freeze.pattern_type),
99        freeze.scope,
100        freeze.set_by,
101        freeze.set_at_ms,
102        freeze.reason,
103    );
104}
105
106/// One block per proposal.
107pub fn print_proposal(proposal: &bg::DescribedBreakGlassProposal) {
108    let state = if proposal.withdrawn {
109        "withdrawn"
110    } else if proposal.consumed_at_ms > 0 {
111        "consumed"
112    } else {
113        "open"
114    };
115    println!(
116        "proposal {} {state}",
117        uuid::Uuid::from_bytes(proposal.proposal_id.0)
118    );
119    println!("  action     {}", action_name(proposal.action));
120    println!("  target     {}", proposal.target);
121    println!("  proposer   {}", proposal.proposer);
122    println!("  reason     {}", proposal.reason);
123    println!("  created    {}", proposal.created_at_ms);
124    println!("  expires    {}", proposal.expires_at_ms);
125    for approval in &proposal.approvals {
126        println!(
127            "  approved   {} at {} {}",
128            approval.principal,
129            approval.approved_at_ms,
130            approval_evidence(approval)
131        );
132    }
133}
134
135/// What one approval offers as evidence of who made it.
136///
137/// The broker never stores a signature it did not check, so a `key_id` on a
138/// stored approval is already proof that the signature verified there. The
139/// first bytes of the signature are printed so an auditor can line one approval
140/// up against the audit event that carries the whole of it.
141fn approval_evidence(approval: &bg::BreakGlassApproval) -> String {
142    /// How many leading signature bytes identify one approval in a report.
143    const PREFIX: usize = 8;
144
145    if approval.key_id.is_empty() {
146        return "unsigned".to_owned();
147    }
148    format!(
149        "signed by {} ({}...)",
150        approval.key_id,
151        hex::encode(
152            approval
153                .signature
154                .get(..PREFIX)
155                .unwrap_or(&approval.signature)
156        )
157    )
158}
159
160/// What a local verification of the registry found.
161///
162/// A signature that failed outranks a key this machine does not hold, because
163/// the first says the tool checked and the answer is wrong, and the second says
164/// the tool could not check.
165pub fn report_verify(freezes: &[api::DescribedTopicFreeze], outcome: &VerifyOutcome) -> i32 {
166    for (freeze, checked) in freezes.iter().zip(&outcome.entries) {
167        print_freeze(freeze, Some(checked));
168    }
169    if outcome.any_signature_failed() {
170        eprintln!("the registry does not verify against the operator keys on this machine");
171        return EXIT_BAD_SIGNATURE;
172    }
173    if outcome.any_key_is_unknown() {
174        eprintln!("the registry names an operator key that this machine does not hold");
175        return EXIT_MISMATCH;
176    }
177    println!(
178        "{} of {} entries are proved by an operator signature",
179        outcome.proved(),
180        outcome.entries.len()
181    );
182    0
183}
184
185#[cfg(test)]
186mod tests;