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
13 changes: 12 additions & 1 deletion src/lab/artifacts/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ const HOSTNAME_RE = /(?<![\w.-])(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.){1,8}(
// Markers differ in confidence, and treating them alike cost accuracy both
// ways. STRONG markers are resolver/socket errors and `host=`: whatever
// follows is a host by construction, so a bare `redis` or `localhost` counts.
// `connect to` additionally requires a host-shaped value, port, or an immediate
// network-failure term before a bare destination is accepted.
// WEAK markers appear in ordinary prose (`upstream provider.metric.p95
// exceeded`), so they only redact a candidate that is already host-shaped.
const STRONG_HOST_CONTEXT_RE =
Expand Down Expand Up @@ -511,8 +513,17 @@ function scrubString(value: string): string {
if (ported?.[1] && !PROSE_AFTER_MARKER.has(ported[1].toLowerCase())) {
return m.replace(ported[1], "[host]");
}
// A failure immediately following an unported connect-to target supplies
// the missing network context without making ordinary connective prose
// (`connect to your account`) host-bearing.
const failedConnectTarget = /\bconnect(?:ing)?\s+to\b/i.test(m)
? tail.match(/^([A-Za-z][A-Za-z0-9]{0,254})\s+(?:failed|refused|unreachable|reset|timed\s+out)\b/i)
: null;
if (failedConnectTarget?.[1]) {
return m.replace(failedConnectTarget[1], "[host]");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace the destination occurrence within the captured tail

For a valid bare destination whose label also occurs earlier in the marker—for example, connect to connect failed or connect to con failedString.replace rewrites the first occurrence in all of m, producing output such as [host] to connect failed and leaving the actual destination in the durable Lab text. Replace at the captured tail's offset, and add a repeated-label regression case, so the destination occurrence itself is always redacted.

Useful? React with 👍 / 👎.

}
// Otherwise only a resolver marker licenses a bare name. Natural-language
// `connect to` does not: `Unable to connect to your account` is prose.
// `connect to` alone does not: `Unable to connect to your account` is prose.
const resolver = /ENOTFOUND|EAI_AGAIN|lookup|host/i.test(m);
for (const token of tail.split(/[\s:]+/)) {
if (token && isHostCandidate(token, resolver)) return m.replace(token, "[host]");
Expand Down
6 changes: 3 additions & 3 deletions structure/09_compatibility-lab.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ host-shaped, so `ETIMEDOUT request after 30 seconds` is untouched.

**Weak** markers (`upstream`, `connect to`) read as English at least as often as
they name a host, so they redact only a candidate that is already host-shaped
and is not a plain dotted namespace. `upstream provider.metric.p95 exceeded` and
`Unable to connect to your account` both survive.
and is not a plain dotted namespace. For `connect to`, an immediately following
network failure term also makes a bare target unambiguous, so `connect to
gateway failed` is redacted while `Unable to connect to your account` survives.

### Known limits

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 |
| 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 |
Expand Down
21 changes: 10 additions & 11 deletions tests/lab-evidence-sanitization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,14 +231,13 @@ describe("SEC-02 sanitizer boundary", () => {
});

test("marker confidence decides how much context is trusted", () => {
// STRONG markers — resolver and socket errors, `host=`, `connect to` —
// name a destination by construction, so even a bare word is a host.
// STRONG resolver and socket markers name a destination by construction,
// so even a bare word is a host.
expect(sanitizeDiagnostic("dial tcp localhost:11434: connect: refused"))
.toBe("dial tcp [host]:11434: connect: refused");
expect(sanitizeDiagnostic("getaddrinfo ENOTFOUND redis")).toBe("getaddrinfo ENOTFOUND [host]");
// `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.
// `connect to` reads as English too often to license a bare word on its own
// (`Unable to connect to your account`); a failure term disambiguates it.
// WEAK markers appear in prose, so a dotted namespace after one survives.
expect(sanitizeDiagnostic("upstream provider.metric.p95 exceeded"))
.toBe("upstream provider.metric.p95 exceeded");
Expand Down Expand Up @@ -291,16 +290,16 @@ describe("SEC-02 sanitizer boundary", () => {
.toBe("connect to [host] port 8080 failed");
});

test("natural-language connect-to prose is left alone", () => {
// `connect to` reads as English far more often than as a destination, so
// it no longer licenses a bare word. The cost is that `connect to gateway`
// is not redacted; that limit is documented and asserted here so it cannot
// change silently.
test("connect-to failures redact destinations without consuming prose", () => {
// `connect to` reads as English far more often than as a destination. A
// following failure term supplies network context for a bare destination,
// while connective prose remains intact.
expect(sanitizeDiagnostic("Unable to connect to your account. Please retry."))
.toBe("Unable to connect to your account. Please retry.");
expect(sanitizeDiagnostic("failed to connect to the upstream service"))
.toBe("failed to connect to the upstream service");
expect(sanitizeDiagnostic("connect to gateway failed")).toBe("connect to gateway failed");
expect(sanitizeDiagnostic("connect to gateway failed")).toBe("connect to [host] failed");
expect(sanitizeDiagnostic("connecting to redis timed out")).toBe("connecting to [host] timed out");
});

test("an internationalized domain does not preserve the address", () => {
Expand Down
Loading