Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ recon "<domain>"
Synthetic Alpha Ltd
alpha.invalid
──────────────────────────────────────────────────────────────────────────────
Provider Microsoft 365 (MX delivery path) + Proofpoint gateway (MX delivery path)
Provider Microsoft 365 + Proofpoint gateway
Tenant a1b2c3d4-e5f6-7890-abcd-ef1234567890 • NA
Auth Federated
Confidence ●●● High (4 sources)
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ operator, corporate group, ownership, or control.
catches contract drift without tracking tenant churn. No shipped behavior
changed: `SourceResult` already recorded the negative with
`source_unavailable` false, and that field is what merge and delta consume.
- The default human views no longer repeat an evidence-role qualifier on every
service and provider label. `Slack (public TXT account indicator)`,
`Okta (CNAME endpoint binding)`, and `Microsoft 365 (MX delivery path)` now
read as `Slack`, `Okta`, and `Microsoft 365` on the default panel, `--plain`,
`--md`, and the `--chain` tree, with one dim line naming the flag that
restores the detail. `--explain`, `--verbose`, and `--full` are unchanged, as
are the `--json` record (including the stable `provider` field), `--csv`, and
every MCP payload. A match whose role could not be established is omitted
from the default view and counted in that line rather than rendered as a bare
vendor name, so removing the qualifier never upgrades a claim; the provider
row re-hedges as `(no supporting record)` instead of dropping, and the
downstream and unobserved-gateway hedges survive in shortened form. Recorded
in [ADR-0012](docs/adr/0012-default-view-evidence-role-visibility.md), which
also restates the renderer obligations on the `runtime.service-label.v1` and
`runtime.email-topology.v1` claim families.
- Insight curation moved out of `formatter/panel.py` into
`formatter/insight_curation.py`. It is editorial policy over insight strings
rather than rendering, it depends on nothing else, and the move ratchets the
panel module's file-size ceiling down from 1719 to 1615 lines. No behavior
change.

## [2.10.2] - 2026-08-04

Expand Down
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,23 +80,21 @@ $ recon globex.invalid
Globex Ltd
globex.invalid
──────────────────────────────────────────────────────────────────────────────
Provider Microsoft 365 (MX delivery path) + Proofpoint gateway (MX
delivery path)
Provider Microsoft 365 + Proofpoint gateway
Tenant a1b2c3d4-e5f6-7890-abcd-ef1234567890 • NA
Tenant domain globex.onmicrosoft.invalid
Auth Federated
Confidence ●●● High (4 sources)


Services
Email Microsoft 365 (MX delivery path),
Proofpoint (MX delivery path), DMARC reject, DKIM,
Email Microsoft 365, Proofpoint, DMARC reject, DKIM,
SPF strict, MTA-STS enforce
Identity Okta (CNAME endpoint binding)
Identity Okta
Cloud Cloudflare (CDN/edge), AWS Route 53 (DNS)
Security Wiz Security (public TXT account indicator)
Collaboration Slack (public TXT account indicator),
Atlassian (Jira/Confluence) (CNAME endpoint binding)
Security Wiz Security
Collaboration Slack, Atlassian (Jira/Confluence)
Evidence roles: --explain


High-signal related domains
Expand Down
3 changes: 3 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ exploit checks.
**Claim discipline** - domain is a query coordinate; sparse stays sparse;
source failure is unavailable, never negative; parent platform is not
child-product use; public-evidence index is not overall security maturity.
Evidence roles are qualified on the detail and machine surfaces and may be
compacted out of the default view, never in a way that upgrades a claim
([ADR-0012](docs/adr/0012-default-view-evidence-role-visibility.md)).

**Data handling** - no committed real-target corpus; synthetic / reserved
examples; aggregate-only public validation artifacts.
Expand Down
2 changes: 1 addition & 1 deletion agents/claude-code/skills/recon/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ recon "<domain>"
Synthetic Alpha Ltd
alpha.invalid
──────────────────────────────────────────────────────────────────────────────
Provider Microsoft 365 (MX delivery path) + Proofpoint gateway (MX delivery path)
Provider Microsoft 365 + Proofpoint gateway
Tenant a1b2c3d4-e5f6-7890-abcd-ef1234567890 • NA
Auth Federated
Confidence ●●● High (4 sources)
Expand Down
75 changes: 75 additions & 0 deletions docs/adr/0012-default-view-evidence-role-visibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# ADR-0012: Split evidence-role visibility between the default and detailed views

- **Status:** Accepted
- **Date:** 2026-08-07

## Context

Every human-facing service label carried the record role that established it,
inline and in full prose: `Slack (public TXT account indicator)`,
`Okta (CNAME endpoint binding)`, `Microsoft 365 (MX delivery path)`,
`Yahoo Small Business (role unavailable)`. The renderer obligation on claim
family `runtime.service-label.v1` required the qualification, and the
obligation is right: a catalog name alone does not establish a role, and a TXT
account record is not a deployment.

The implementation of that obligation was wrong for the default view. The
qualifier repeated once per service, and often several times per line, so the
answer to "what do they run" arrived buried under the answer to "how do we
know", which the operator had not asked for yet. On a dense apex the Provider
row spent two wrapped lines saying `(MX delivery path)` twice. The
`(role unavailable)` case was worse than noise: it occupied a row to report
that recon could not say why it named the vendor at all.

The claim-discipline invariant in `ROADMAP.md` treats this qualification as a
default-surface obligation, so relaxing it needs a decision record rather than
a rendering tweak.

## Decision

Evidence roles move from "always inline" to "in the view that asks for them".

- The **default** panel, `--plain`, `--md`, and the `--chain` tree render
service and provider labels without their record-role qualifier.
- `--explain`, `--verbose`, and `--full` render every qualifier exactly as
before, unchanged byte for byte.
- Every **machine** surface is unchanged: the `--json` record (including the
stable `provider` field), `--csv`, and the MCP tool payloads keep the full
prose roles. Agents and scripts lose nothing.
- A label whose role is **unavailable** is *omitted* from the default view
rather than rendered bare. Stripping the qualifier in place would promote an
unattributed catalog match into an asserted observation, which inverts the
obligation instead of relocating it.
- Two qualifiers survive compaction in shortened form because they hedge the
claim rather than name a record type: a non-MX provider renders as
`(likely downstream)`, and a gateway with no observed downstream renders as
`(downstream unobserved)`.
- The provider row re-hedges instead of dropping, as `(no supporting record)`.
It is the panel's single answer to "who handles their mail"; dropping the
segment would leave the row silent, and rendering it bare would assert a
delivery path no retained record supports.
- A default view that left something out says so, once, pointing at
`--explain`, and naming `--full` with a count when unattributed matches were
omitted. A view that compacted nothing carries no note.

The obligation on `runtime.service-label.v1` and `runtime.email-topology.v1`
is restated accordingly: roles must be qualified on the evidence surfaces and
must never be *implied* on the default surface. Compaction that would upgrade
a claim is prohibited; compaction that only relocates a role is required.

## Consequences

- The default view answers the question the operator asked. Role detail is one
documented flag away, and the panel says which flag.
- The claim model is unchanged in substance. No default-view label asserts more
than it did before; the unattributed case now asserts strictly less.
- The default view is no longer a complete enumeration of catalog matches.
Unattributed matches appear only under `--full` and in the JSON record, so
any consumer counting rows must use the JSON record, not the panel.
- `--json` and the panel now differ in wording by design. `docs/stability.md`
already excludes panel prose from the contract and keeps the JSON `provider`
field stable; this ADR is the record of that divergence being deliberate.
- A new role qualifier must be added to the compaction table in
`formatter/classify.py` at the same time it is added to the label producers,
or it will leak into the default view. The golden renders in
`tests/test_golden_renders.py` are the regression guard.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ deeper rationale for several lives in [correlation.md](../correlation.md),
| [0009](0009-mcp-2026-readiness.md) | Prepare for MCP 2026-07-28 without premature protocol forking |
| [0010](0010-evidence-gated-native-acceleration.md) | Keep the default runtime pure Python and evidence-gate any optional native accelerator |
| [0011](0011-public-metadata-collection-boundary.md) | Define the public-metadata collection boundary and target-visible interactions |
| [0012](0012-default-view-evidence-role-visibility.md) | Split evidence-role visibility between the default and detailed views |
Loading
Loading