diff --git a/crates/nanny-entities/src/rule.rs b/crates/nanny-entities/src/rule.rs index 8cf298f..61863cd 100644 --- a/crates/nanny-entities/src/rule.rs +++ b/crates/nanny-entities/src/rule.rs @@ -281,7 +281,7 @@ mod tests { #[test] fn severity_ranks_critical_first() { - let mut v = vec![Severity::Low, Severity::Critical, Severity::Medium]; + let mut v = [Severity::Low, Severity::Critical, Severity::Medium]; v.sort_by_key(|s| s.rank()); assert_eq!(v[0], Severity::Critical); } diff --git a/doc/rotation.md b/doc/rotation.md new file mode 100644 index 0000000..b2f53e1 --- /dev/null +++ b/doc/rotation.md @@ -0,0 +1,99 @@ +# Rotation + +**Status: design note. Not implemented.** nanny records and alerts; the operator +decides what to do. This document exists so that when optional rotation is +built, it lands on a seam that is already in the right place rather than being +retrofitted through the middle of the detection path. + +## Why it is not built yet + +The expensive mistake would be to build rotation before the detection is +trusted. A tool that revokes a credential on a false positive costs more than +one that misses a real leak, because the false positive breaks something that +was working and the operator learns to distrust the tool. Detection quality has +to be demonstrated on a real corpus first — and the first sweep of a real +workstation produced 1538 findings, most of them fixtures in vendored +dependencies, which is exactly the state in which automated revocation would +have been a disaster. + +So the order is: detect, tune until the findings are believable, then automate +the response. + +## What is already in place + +**`RotationHint`**, carried by a rule rather than by a finding, because what you +do about a leaked forge token does not depend on which transcript it turned up +in: + +```rust +pub struct RotationHint { + pub service: String, // "anthropic", "gitea", "aws" — the dispatch key + pub instructions: String, // what a human does, right now + pub docs: Option, + pub rotation_suffices: bool, // false ⇒ incident response, not rotation +} +``` + +`rotation_suffices` is the field that will matter most. A leaked API key is +undone by revoking it. A leaked private key is not: the key has to be replaced +*and* the old public key removed from every trust store it reached, and no +amount of automation makes that a one-button operation. Rules where this is +`false` must never be handed to an automatic executor. + +**`Status::Rotated`**, distinct from `Status::Acknowledged`. Acknowledged means +the operator has seen it and decided; rotated means the leaked value is now +worthless. Only the second closes the exposure, and the UI keeps them apart so +that a triaged backlog is never mistaken for a clean one. An executor sets +`Rotated`; nothing else should. + +**The `Fingerprint`.** Because the same secret in five places shares one +fingerprint, a single rotation can resolve every finding carrying it. That is +the unit of work an executor should operate on — a secret, not a location. + +## What it would take + +An `Remediator` port in `nanny-core`, alongside `Alerter`, dispatching on +`RotationHint::service` to per-service implementations. Sketch: + +```rust +#[async_trait] +pub trait Remediator: Send + Sync { + fn service(&self) -> &str; + /// What this would do, without doing it. + async fn plan(&self, finding: &Finding) -> Result; + /// Do it. Only ever reached after explicit operator confirmation. + async fn execute(&self, plan: &RemediationPlan) -> Result; +} +``` + +Four constraints that follow from the rest of the design, and that a future +implementation does not get to relax: + +1. **An executor cannot read the leaked value, because nothing stores it.** It + therefore cannot revoke "the credential that leaked" by presenting it. It + revokes *by identity at the service* — the key with this prefix, the token + named for this consumer — which is why `agent-credentials.md` §5's practice + of naming issued credentials after their consumer matters here. +2. **Rotation needs credentials, and nanny currently has none.** This is the + real cost of the feature: nanny becomes a process holding provisioning + credentials, which is the thing the patterns-only detection design exists to + avoid. The mitigation is the mint-don't-share pattern of + `agent-credentials.md` §2 — a narrow provisioning credential per service that + can revoke and reissue and nothing else — and it should be opt-in per + service, off by default, never a blanket capability. +3. **Never automatic.** `plan` then confirm then `execute`. A monitor that + revokes credentials on its own judgement is a denial-of-service with good + intentions. +4. **Still never edits a harness's files.** Rotating makes the leaked copy + inert, which is the correct remedy. Rewriting a live session transcript to + scrub it risks breaking the harness that is writing it, and leaves the + operator believing a copy is gone when backups, the model provider's logs and + `file-history/` still hold it. + +## The thing rotation does not fix + +A credential that reached a model provider's servers is gone, whatever happens +locally afterwards. Rotation is the only remedy that works, which is the whole +argument for detecting the spill in the first place — and for `nanny scan` +exiting non-zero, so it can gate something before the transcript is uploaded +rather than after.