Phase 2: Detection - #1
Merged
Merged
Conversation
A detector cannot honestly report a missing security header unless it can tell an absent header from one RedDock never looked for, so the response observation now states the header set that was examined alongside the headers that were present, and the examined set covers the response-level protections a Phase 2 detector needs. Certificate verification failures now record the code and message OpenSSL gave. An unverified handshake returns an empty peer certificate, so without the verification reason there is nothing to say about a certificate beyond "it did not verify". The probe User-Agent is also derived from the application version rather than repeated as a literal, which is what the 0.2.1 changelog claims and what would otherwise drift at the next release. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detection turns observations into findings without letting either concept absorb the other. An observation stays a dated statement of what an adapter saw; a finding is a normalized conclusion one named detector drew, and it cannot exist without the observations that support it. The detector contract is deliberately weaker than the discovery adapter contract. A detector receives a frozen snapshot of one Dockyard and returns value objects. It gets no session, no socket, no subprocess, no target and no operator input, and tests read the package to prove it rather than trusting the docstring. Validation, identity, reconciliation, resolution and evidence all belong to the runner, so a detector that returns something malformed is failed as a whole rather than half-believed. Identity is a SHA-256 fingerprint over detector, rule and the asset and service the finding is about, which keeps one issue as one row across runs, restarts and processes. A finding that stops being reproduced is resolved rather than deleted, and a decision an operator made about it is never overwritten by a later run. The first three detectors only say what Phase 1 data actually supports. Missing security headers are reported only for headers the probe examined, only for responses that represent how an endpoint normally answers, and at restrained severity. Service rules run off protocols RedDock identified, not port numbers it recognised. TLS reports what certificate verification objected to, and deliberately says nothing about protocol versions, because the probe negotiates with a current client and could never observe an obsolete one. CVE enrichment ships as a boundary with a local catalogue behind it. RedDock downloads nothing, matches only an exact product and version, records where the association came from, and never lets a match change a severity, a confidence or a status. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Detection reads stored state and contacts nothing, so a run completes inside the request rather than leaving something in flight to recover after a restart. The request body is empty by design: there is no target, no detector selection and no option, so there is no operator-supplied value for a detector to act on. Findings are filterable by status, severity, detector, asset and service, with every filter validated before it reaches a query and the Dockyard always part of it. An operator may suppress, accept or reopen a finding but never declare it resolved, because whether an issue is still reproduced is a fact about the data rather than an opinion about it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The React workspace gains two sections rather than a new application: Detection, which states what a detection run reads and what each registered detector consumes before running one, and Findings, which lists what came out of it. The presentation stays restrained deliberately. Severity and confidence are separate columns, not one blended number; there is no risk score, no gauge and no aggregate rating, because RedDock does not compute one. Opening a finding shows the detector and rule that produced it, the observation it was drawn from, the discovery and detection runs involved and the SHA-256 of the retained artifact, so the reader can follow the claim back to the evidence instead of trusting the label. Screenshots are recaptured against a real local container so the documentation shows v0.3.0 rather than a release the product has moved past. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Aligns the application, API, and both package manifests on 0.3.0 and moves the reported phase to Detection, with a test asserting that alignment so the next release cannot drift the way the last one nearly did. Documentation now describes what is built rather than what is planned. The README hero claim about evidence-backed findings is finally true, so it stays; the roadmap moves Phase 2 to complete and names Phase 3 as next; the architecture gains the detection boundary, the finding lifecycle, the detection half of RedLedger, and the reason detection artifact hashes live on the run rather than in evidence_records. SECURITY.md gains a detection control list and is explicit that RedDock still performs no active vulnerability testing: detection concludes from what was recorded, it does not confirm by attempting. Two ADRs record the decisions that were not obvious: that a detector concludes but never reaches, and that a CVE association is enrichment rather than a conclusion. The smoke test now proves the whole path end to end in CI, against RedDock's own origin inside the container. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A detection run completes inside its request, so one still marked active at startup did not finish. Leaving it that way would have been worse than untidy: an overlapping run is refused while one looks active, so a run left behind by a crash would have blocked that Dockyard's detection permanently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deduplication keys on the fingerprint, so a detector emitting the same rule twice for one service would quietly overwrite its own first finding and leave the run reporting more findings than exist. That is a detector failing to distinguish two things it believes are different, which is a scope key it did not set, so it is refused rather than absorbed. The three shipped detectors cannot hit this — each rule fires at most once per endpoint — but the check belongs with the other output validation rather than waiting for the fourth detector to find it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The findings page has a workspace picker, and the detail panel kept whatever was open across a change of Dockyard. In a product whose central promise is that workspaces are isolated, showing one workspace's finding beside another workspace's list is exactly the wrong thing to render. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… pytest The Phase 2 test modules share a helper through `from tests.phase1 import Recorder`, which only resolves when the working directory is on sys.path. That is true for `python -m pytest` and false for the bare `pytest` CI runs, so four modules failed to collect there while passing locally. Adding the package marker fixes the import under any invocation. The lesson is the local check, not the missing file: verification has to run the command CI runs, so this was reproduced with `pip install -e ".[dev]"` and bare `ruff`/`pytest` before and after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 gives RedDock findings without letting them absorb observations. An observation still states what an adapter saw. A finding is a separate thing: a normalized conclusion one named detector drew from one or more observations, which cannot exist without them.
The detector boundary
A detector is deliberately weaker than a discovery adapter. An adapter may contact a target after DockGuard allows it; a detector may not contact anything. It receives an immutable snapshot of one Dockyard and returns value objects — no database session, no socket, no subprocess, no target string, no operator-supplied option. There is nothing for it to widen, execute, or reach, which is why it needs no scope decision.
That is enforced structurally rather than by convention.
tests/test_detection_contract.pyparses the detection package and fails the build if a detector imports anything that could leave the process or touch the database, and asserts that nothing in detection callseval,exec,compile, or__import__.Everything a detector could get wrong belongs to the runner: building the snapshot, validating the output, computing identity, reconciling, resolving, writing evidence. A detector that raises, returns an unknown severity, uses an unusable rule id, names another Dockyard's data, or cites no observation is failed as a whole and its results are discarded. The other detectors still run, and a failed detector resolves nothing — not running is not evidence that an issue went away.
Two invariants
A finding must cite evidence. A conclusion with nothing behind it is refused, not stored with a caveat.
FindingEvidencelinks each finding to the observations it was drawn from, their discovery run, and that run's hashedEvidenceRecord, so "which detector, from what observation, during which run, verified by which hash" is answerable without leaving the database.A finding is never deleted. Identity is a SHA-256 fingerprint over the detector, the rule, and the asset and service concerned — deterministic across runs, restarts, and processes, which Python's randomized
hash()would not be. One issue stays one row whoselast_seenmoves. An issue a later successful run no longer reproduces is markedresolved; an operator maysuppress,accept, or reopen one but may not declare it resolved, because whether something is still reproduced is a fact about the data rather than an opinion about it.Detectors
Three, and each says only what Phase 1 data actually supports.
http.security_headersservice.rulestls.certificatesThe false-positive controls are the point:
low, and severity and confidence stay separate fields.What is deliberately absent: there is no TLS protocol-version rule. The probe negotiates with a default client, so it can only ever record a version a current client accepted, and a rule about obsolete versions could never fire from RedDock's own data. Shipping it would suggest a capability that does not exist.
CVE enrichment
RedDock downloads no CVE data. This ships the boundary plus a local catalogue reader behind
REDDOCK_CVE_CATALOG, off by default.An association never creates a finding, never changes a severity, confidence, or status, and attaches to the version-disclosure finding that already stood on its own evidence. Matching is exact product and version only — version ranges are not interpreted, because an inference printed beside a CVE identifier reads as a result. A missing or malformed catalogue is a warning on the run, not a failure, and each run records which enrichment source was in effect so a finding with no CVE reference can be told apart from one RedDock could not enrich. A test asserts the same data produces the same rating with and without a catalogue loaded.
The limitation is documented rather than hidden: out of the box, no finding carries a CVE reference. See ADR 0007.
Preflight fixes
Three issues found before building on top of the existing code, all in the same branch:
verified: falsealone supports no claim about a certificate. The probe now records the code and message OpenSSL gave.RedDock/0.2.1, which the 0.2.1 changelog claims tracks the application version. It now derives from settings, and a test asserts the application, API, and both package manifests report one version.Schema
Purely additive: three new tables, no existing column changed, so
create_allupgrades a deployed database in place.tests/test_schema_upgrade.pynow builds a real 0.2.1-shaped database, runs a full detection over data the previous release wrote, and asserts every Phase 1 table keeps the shape it had.That constraint shaped a decision rather than merely being stated. Detection artifact hashes live on the detection run because
evidence_records.discovery_run_idis NOT NULL and relaxing it would be the first destructive schema change — which this architecture already says arrives with versioned migrations rather than an ad hoc alteration. Both halves are written by the same store, under the same root, both SHA-256 hashed.Validation
dangerouslySetInnerHTMLwas introducedSafety
Phase 2 adds no exploitation, credential attack, brute force, payload execution, evasion, persistence, lateral movement, or autonomous behaviour, and no active vulnerability testing. Detection reasons over data an earlier non-invasive discovery recorded; it sends nothing and confirms nothing by attempting it. DockGuard is unchanged and remains authoritative for everything that does reach a target. No AI is involved: every detector is a deterministic rule, and the same input produces the same findings.
🤖 Generated with Claude Code