Skip to content
Draft
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
25 changes: 9 additions & 16 deletions src/lab/artifacts/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,11 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-
// must be consistent within one address.
const MAC_RE = /(?<![0-9A-Za-z:-])(?:(?:[0-9A-Fa-f]{2}:){5}|(?:[0-9A-Fa-f]{2}-){5})[0-9A-Fa-f]{2}(?![0-9A-Za-z:-])/g;
const IPV4_RE = /(?<![0-9A-Za-z.])(?:\d{1,3}\.){3}\d{1,3}(?![0-9A-Za-z.])/g;
// The final label is either an ordinary alphabetic TLD or a punycode one.
// Allowing hyphens and digits in every final label was too broad: it ate
// ordinary diagnostic tokens such as `foo.bar-baz` and `metric.p95`, which
// costs exactly the evidence quality the Lab exists to capture.
// A trailing `-` or word character after the TLD means this was never a
// hostname (`foo.bar-baz`), so the lookahead refuses the match. A trailing
// `.` is allowed and consumed: it is FQDN root notation and equally ordinary
// sentence punctuation, and forbidding it let `api.example.com.` through
// untouched.
// Match the complete DNS label grammar, including private suffixes containing
// digits or hyphens. Provider-controlled diagnostics are durable evidence, so
// an ambiguous dotted token must be treated as a hostname rather than risk
// persisting an internal network identifier. A trailing `.` is allowed and
// consumed for FQDN root notation.
//
// The leading `(?<![\w.-])` matters just as much: without it the engine
// backtracks to a later starting label and redacts a prefix, turning
Expand All @@ -139,16 +135,13 @@ const IPV4_RE = /(?<![0-9A-Za-z.])(?:\d{1,3}\.){3}\d{1,3}(?![0-9A-Za-z.])/g;
// `[host].p95`. The terminator is therefore mandatory and expressed as a
// single alternation — either the token ends cleanly, or it ends with an FQDN
// root dot that is itself followed by nothing label-shaped.
const HOSTNAME_RE = /(?<![\w.-])(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){1,8}(?:xn--[a-z0-9-]{1,55}|[a-z]{2,24})(?:\.(?![\w-])|(?![\w.-]))/gi;
const HOSTNAME_RE = /(?<![\w.-])(?!v?[0-9.]+(?:\.|$))(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){1,8}[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.(?![\w-])|(?![\w.-]))/gi;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Anchor the numeric-version exclusion to the whole token

When a private hostname starts with a numeric or version-like label, such as 1.internal, 123.example.com, or v2.internal, the new negative lookahead matches only the initial 1./v2. prefix and rejects the entire hostname. sanitizeDiagnostic consequently preserves these values verbatim even though the previous regex redacted them, allowing provider-controlled durable evidence to persist an internal hostname. Make the exclusion require the entire candidate to be a numeric version rather than accepting its first dot, and add numeric-leading hostname regression cases.

Useful? React with 👍 / 👎.

// The value is scanned to its delimiter, not to a word boundary: `\b` stops at
// the first `.` in `db_prod.internal`, which left `[host].internal` — a partial
// redaction that still names the host.
// `db.prod-1` and `api.us-east-1` are indistinguishable from ordinary dotted
// diagnostics by shape alone — a numeric or hyphenated final label is both a
// legitimate internal hostname and a legitimate metric or version namespace.
// Shape cannot decide it, so context does: these forms are redacted only when
// an unambiguous network marker introduces them. Outside such a marker they
// survive, and that is a recorded limit rather than an oversight.
// Context rules additionally cover single-label names. Multi-label names are
// already handled above, including ambiguous metric or version namespaces,
// because durable provider-controlled evidence must fail closed.
//
// The marker grammar accepts the separators these messages actually use —
// whitespace, `=`, `:`, and JSON quoting — and the CANDIDATE IS VALIDATED
Expand Down
25 changes: 12 additions & 13 deletions structure/09_compatibility-lab.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,13 @@ Redacted: filesystem paths including UNC shares, HTTP(S) URLs, credential-bearin
and other-scheme URIs, JWT-shaped tokens, email addresses including internationalized local parts and domains, prefixed account
identifiers (`acct_`, `cus_`, `sub_`, `org-`), account values under an
ID-bearing label (`user_id`, `userID`, `organization_id`, `accountId`, …; matched case-insensitively), MAC addresses in either colon or hyphen notation, IPv4,
IPv6 including mapped and scoped forms, and multi-label hostnames whose final
label is alphabetic or punycode.
IPv6 including mapped and scoped forms, and multi-label hostnames, including
private suffixes containing digits or hyphens.

**Hostname limit.** A final label containing digits or hyphens — `db.prod-1`,
`api.us-east-1` — is simultaneously a valid internal hostname and a valid
metric or version namespace (`provider.metric.p95`, `lib.v2-rc1`). Shape cannot
separate them. Those forms are therefore redacted only when an unambiguous
network marker introduces them, and survive otherwise.
Multi-label hostnames and dotted metric or version namespaces have overlapping
grammar (`db.prod-1`, `provider.metric.p95`, `lib.v2-rc1`). Provider-controlled
text is persisted as durable evidence, so ambiguous dotted tokens fail closed
and are redacted as hostnames even without a network marker.

Markers carry two confidence levels, because treating them alike lost accuracy
in both directions.
Expand Down Expand Up @@ -60,17 +59,17 @@ Recorded rather than implied, so a reader knows what is not covered:
| Form | Behavior |
|------|----------|
| Bare service name after natural-language `connect to` with no port at all (`connect to gateway failed`) | not redacted — the phrase is prose too often to trust. A port in either notation (`gateway:443`, `gateway on port 443`) does make it a host |
| Bare `db.prod-1` outside any network context | not redacted — indistinguishable from a metric namespace |
| Dotted metric or version namespace (`provider.metric.p95`, `lib.v2-rc1`) | **over-redacted to `[host]`** — indistinguishable from a real internal hostname |
| Standalone UUID, standalone `user_…`, bare-label value (`org: engineering`) | not redacted — indistinguishable from request, trace, and correlation ids |
| Phone numbers, generic high-entropy blobs | not redacted — no non-destructive pattern |
| Cisco dotted MAC (`0123.4567.89ab`), ideographic-dot IDN | not redacted — unusual notations |
| Escaped-quote mail local part | partially redacted; the address is broken but a fragment of the local part can remain |
| Percent-encoding nested more than six deep | not decoded further |
| Fully alphabetic dotted namespace (`provider.timeout`, `provider.request.duration`) | **over-redacted to `[host]`** — indistinguishable from a real hostname. A namespace whose last label carries a digit (`provider.metric.p95`) survives |
| Fully alphabetic dotted namespace (`provider.timeout`, `provider.request.duration`) | **over-redacted to `[host]`** — indistinguishable from a real hostname |

The marker behaviors and the redacted categories are asserted in both
directions — positive cases for what must be removed, negative cases for the
ordinary diagnostics that must survive — so those cannot drift silently. The
non-host diagnostics that must survive — so those cannot drift silently. The
limits table is a description of current behavior; only the entries with a
matching test are pinned, and the unusual-notation rows are not.
A retained URL path also has identifier-shaped content redacted wherever it
Expand All @@ -94,9 +93,9 @@ leaks. `enforceEventStructureLimits` remains a backstop that rejects
secret-shaped strings and raw paths; it is not the enforcement point.

Both directions are enforced by tests: every redacted category has a positive
case, and ordinary dotted diagnostics (`provider.metric.p95`, `lib.v2-rc1`,
`foo.bar-baz`) have negative cases, because a sanitizer that destroys evidence
fails this contract as surely as one that leaks it.
case, while non-host diagnostics have negative cases. Ambiguous dotted
diagnostics are deliberately redacted because confidentiality takes precedence
when provider-controlled text becomes durable evidence.

Non-contract artifacts declare `redactionPolicy: sanitized_evidence_v2`.
Contract classes (fixtures and manifests) bypass mutation, so their pinned
Expand Down
33 changes: 15 additions & 18 deletions tests/lab-evidence-sanitization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ describe("SEC-02 sanitizer boundary", () => {
// documentation cannot quietly stop matching the code.
expect(sanitizeDiagnostic("provider.timeout")).toBe("[host]");
expect(sanitizeDiagnostic("provider.request.duration")).toBe("[host]");
// The moment any label carries a digit it reads as a namespace and lives.
expect(sanitizeDiagnostic("provider.metric.p95")).toBe("provider.metric.p95");
// Numeric labels remain ambiguous and therefore fail closed too.
expect(sanitizeDiagnostic("provider.metric.p95")).toBe("[host]");
});

test("identifiers survive neither punctuation, word boundaries, nor a URL path", () => {
Expand Down Expand Up @@ -206,16 +206,17 @@ describe("SEC-02 sanitizer boundary", () => {
expect(sanitizeDiagnostic("acct_abcdef-prod")).toBe("[account]");
});

test("hostnames with numeric or hyphenated labels redact in a network context", () => {
test("hostnames with numeric or hyphenated labels redact with or without context", () => {
// `db.prod-1` is a valid internal hostname AND a valid metric namespace;
// shape cannot separate them, so an explicit network marker decides.
// shape cannot separate them, so durable evidence fails closed.
expect(sanitizeDiagnostic("dial tcp db.prod-1:443: connect: refused"))
.toBe("dial tcp [host]:443: connect: refused");
expect(sanitizeDiagnostic("upstream api.us-east-1 unavailable"))
.toBe("upstream [host] unavailable");
// Without such a marker they survive — a recorded limit, asserted so it
// cannot drift silently in either direction.
expect(sanitizeDiagnostic("db.prod-1")).toBe("db.prod-1");
// Provider-controlled text is persisted, so ambiguous standalone values
// fail closed rather than exposing an internal hostname.
expect(sanitizeDiagnostic("db.prod-1")).toBe("[host]");
expect(sanitizeDiagnostic("api.internal2")).toBe("[host]");
});

test("the network-context grammar covers real syntax without eating prose", () => {
Expand All @@ -239,9 +240,9 @@ describe("SEC-02 sanitizer boundary", () => {
// `connect to` was demoted to a weak marker: it reads as English too often
// (`Unable to connect to your account`). Without a port it no longer
// licenses a bare name — a documented limit, asserted below.
// WEAK markers appear in prose, so a dotted namespace after one survives.
// A multi-label token still fails closed independently of marker strength.
expect(sanitizeDiagnostic("upstream provider.metric.p95 exceeded"))
.toBe("upstream provider.metric.p95 exceeded");
.toBe("upstream [host] exceeded");
});

test("full-word account labels and both MAC notations are covered", () => {
Expand Down Expand Up @@ -331,20 +332,16 @@ describe("SEC-02 sanitizer boundary", () => {
// provider.metric.p95 regressed to [host].p95.
expect(sanitizeDiagnostic("api.example.com.")).toBe("[host]");
expect(sanitizeDiagnostic("connect to api.example.com.")).toBe("connect to [host]");
expect(sanitizeDiagnostic("provider.metric.p95")).toBe("provider.metric.p95");
expect(sanitizeDiagnostic("provider.metric.p95")).toBe("[host]");
});

test("hostname redaction does not eat ordinary dotted diagnostics", () => {
// Widening the final label for punycode initially swallowed these. A
// sanitizer that destroys evidence fails the Lab's purpose as surely as
// one that leaks it.
test("ambiguous dotted diagnostics fail closed as hostnames", () => {
for (const value of ["foo.bar-baz", "lib.v2-rc1", "release.v2", "metric.p95"]) {
expect(sanitizeDiagnostic(value), value).toBe(value);
expect(sanitizeDiagnostic(value), value).toBe("[host]");
}
// Three-label forms too: the rule must refuse the whole token rather than
// backtracking to a later label and leaving `[host].p95`.
// Three-label forms are consumed whole rather than partially redacted.
for (const value of ["provider.metric.p95", "client.api.v2-rc1"]) {
expect(sanitizeDiagnostic(value), value).toBe(value);
expect(sanitizeDiagnostic(value), value).toBe("[host]");
}
// Real hosts, including punycode, still go.
expect(sanitizeDiagnostic("internal.corp.example")).toBe("[host]");
Expand Down
Loading