diff --git a/docs/process-capture.md b/docs/process-capture.md index d93a968f3..69a39c64f 100644 --- a/docs/process-capture.md +++ b/docs/process-capture.md @@ -112,8 +112,10 @@ same admitted PTY chunks rather than a second unbounded stream. Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and `listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with -ports from 0 through 65535. It preserves ordinary counters, dimensions, file -line numbers, and ambiguous bare host labels. Use explicit literal `patterns` +ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`; +a period followed by a letter or digit continues a decimal, version, or host +name instead. It preserves ordinary counters, dimensions, file line numbers, +and ambiguous bare host labels. Use explicit literal `patterns` for a different application-specific spelling. Set `ports: false` to preserve all endpoint numbers in comparison text. Captures commit the port-normalization version so older broad numeric normalization cannot silently compare as the diff --git a/src/domain/process/processScenario.ts b/src/domain/process/processScenario.ts index 08672cdaf..a23e2bd4e 100644 --- a/src/domain/process/processScenario.ts +++ b/src/domain/process/processScenario.ts @@ -85,7 +85,7 @@ export const processComparisonContract = ( limits: scenario.limits, normalization: scenario.normalization, ...(scenario.normalization.ports - ? { port_normalization_version: "contextual-endpoints-v1" } + ? { port_normalization_version: "contextual-endpoints-v2" } : {}), }); diff --git a/src/process/capture/ProcessNormalization.test.ts b/src/process/capture/ProcessNormalization.test.ts index c28d1e42d..b07a228a3 100644 --- a/src/process/capture/ProcessNormalization.test.ts +++ b/src/process/capture/ProcessNormalization.test.ts @@ -99,6 +99,25 @@ it("normalizes only contextual ports before overlapping PID values and honors di ).toBe(input); }); +it("normalizes ports that end a sentence but not decimal or host continuations", () => { + const input = [ + "Listening on port: 8080.", + "Server at http://localhost:8080. Ready", + "Bound [::1]:3000.", + "listen=9000.\n", + "port: 80.5 localhost:8080.5 localhost:8080.example.test", + ].join("\n"); + expect(normalizeProcessText(input, scenario({}), "/temporary", rootPid)).toBe( + [ + "Listening on port: .", + "Server at http://localhost:. Ready", + "Bound [::1]:.", + "listen=.\n", + "port: 80.5 localhost:8080.5 localhost:8080.example.test", + ].join("\n"), + ); +}); + it("normalizes representative process identities and preserves null, zero, and unrelated numbers", () => { const before = JSON.stringify(samples); const normalized = normalizeProcessSamples(samples, scenario({}), rootPid); diff --git a/src/process/capture/ProcessNormalization.ts b/src/process/capture/ProcessNormalization.ts index d9dd68385..27c7d1f49 100644 --- a/src/process/capture/ProcessNormalization.ts +++ b/src/process/capture/ProcessNormalization.ts @@ -41,9 +41,11 @@ export const normalizeProcessText = ( return normalized; }; +// A port may end a sentence, but a period followed by a word character +// continues a decimal, version, or host name and is not a port boundary. const normalizePortTokens = (value: string): string => { const endpointNormalized = value.replaceAll( - /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\w.@:])/giu, + /(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\w@:]|\.\w)/giu, (match: string, endpoint: string, port: string) => { if (Number(port) > 65_535) return match; const isEndpoint = endpoint.includes("://") @@ -54,7 +56,7 @@ const normalizePortTokens = (value: string): string => { }, ); return endpointNormalized.replaceAll( - /(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?![\w.])/giu, + /(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?!\w|\.\w)/giu, (match: string, prefix: string, port: string) => Number(port) <= 65_535 ? `${prefix}` : match, ); diff --git a/tests/boundary/process/processCaptureNormalization.test.ts b/tests/boundary/process/processCaptureNormalization.test.ts index 9006cd41e..697a06f63 100644 --- a/tests/boundary/process/processCaptureNormalization.test.ts +++ b/tests/boundary/process/processCaptureNormalization.test.ts @@ -63,7 +63,7 @@ itWithCaptureCapability( ), ).toBe(true); expect(capture.manifest.comparison_contract).toMatchObject({ - port_normalization_version: "contextual-endpoints-v1", + port_normalization_version: "contextual-endpoints-v2", }); expect(capture.exit).toMatchObject({ code: 0, reason: "exited" }); expect(capture.cleanup).toMatchObject({