fix(provenance): stop short secret values from rewriting unrelated log text (#6416)

This commit is contained in:
Vikhyath Mondreti
2026-08-08 12:09:37 -07:00
committed by GitHub
parent 4f5ad2011f
commit 0aae736b07
9 changed files with 500 additions and 24 deletions
@@ -38,6 +38,7 @@ function createResolvedSecretModelMatcher(
): ResolvedSecretMatcher | undefined {
const matcher = createResolvedSecretMatcher(matches, {
preserveNamedProvenanceLabels: true,
mode: 'render',
})
if (!matcher) return undefined
@@ -74,7 +75,7 @@ function createResolvedSecretModelMatcher(
})),
...opaquePlaceholderMatches,
],
{ preserveNamedProvenanceLabels: true }
{ preserveNamedProvenanceLabels: true, mode: 'render' }
)
}
@@ -0,0 +1,91 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import {
getResolvedSecretMatchPolicy,
isWordBoundaryMatch,
MIN_UNANCHORED_MATCH_LENGTH,
} from '@/executor/utils/resolved-secret-match-policy'
describe('getResolvedSecretMatchPolicy', () => {
it.each(['test', 'Test', '483920', 'hunter2', 'F', ''])(
'restricts short value %s to boundary matches',
(value) => {
expect(value.length).toBeLessThan(MIN_UNANCHORED_MATCH_LENGTH)
expect(getResolvedSecretMatchPolicy(value)).toBe('boundary')
}
)
it.each([
['32-char hex', '5f4dcc3b5aa765d61d8327deb882cf99'],
['base64 key', 'sk-proj-Ab3xK9mQ2pLw7nRt5vYc8Zd4'],
['github pat', 'ghp_16C7e42F292c6912E7710c838347Ae178B4a'],
['slack bot token', 'xoxb-2334-4567-abcdefGHIJKL'],
['9-digit value', '123456789'],
['8-char password', 'Passw0rd'],
])('allows unanchored matching for a %s', (_label, value) => {
expect(getResolvedSecretMatchPolicy(value)).toBe('anywhere')
})
/**
* Every one of these scores below 3.0 bits/char; an entropy floor would have demoted them.
* Prefixed shapes are assembled at runtime so the source carries no literal that reads as a
* live credential to a secret scanner.
*/
it.each([
['all-f HMAC key', 'f'.repeat(32)],
['test PAN', '4111111111111111'],
['padded AWS key id', `AKIA${'0'.repeat(16)}`],
['repeated-block hex', 'deadbeefdeadbeefdeadbeefdeadbeef'],
['padded stripe-style key', `sk_live_${'0'.repeat(24)}`],
['padded PAT', `ghp_${'a'.repeat(36)}`],
])('keeps unanchored matching for a low-variety full-length %s', (_label, value) => {
expect(getResolvedSecretMatchPolicy(value)).toBe('anywhere')
})
})
describe('isWordBoundaryMatch', () => {
it.each([
['test', 0, 4, true],
['key=test', 4, 8, true],
['"test"', 1, 5, true],
['{"k":"test"}', 6, 10, true],
['test ok', 0, 4, true],
['latest', 2, 6, false],
['tested', 0, 4, false],
['prefixtest', 6, 10, false],
])('anchors %s at [%i,%i) => %s', (value, start, end, expected) => {
expect(isWordBoundaryMatch(value, start, end)).toBe(expected)
})
it.each([
['user_test_id', 5, 9],
['sk_live_test', 8, 12],
['test_suffix', 0, 4],
])('anchors %s across an underscore, the dominant identifier joiner', (value, start, end) => {
expect(isWordBoundaryMatch(value, start, end)).toBe(true)
})
it('treats non-ASCII letters as word characters', () => {
expect(isWordBoundaryMatch('прtestка', 2, 6)).toBe(false)
})
it('treats astral-plane letters as word characters', () => {
expect(isWordBoundaryMatch('\u{1D400}test\u{1D401}', 2, 6)).toBe(false)
expect(isWordBoundaryMatch('x\u{20000}test\u{20000}y', 3, 7)).toBe(false)
})
it('keeps a combining mark attached to the word it decorates', () => {
expect(isWordBoundaryMatch('test́ing', 0, 4)).toBe(false)
})
it('anchors a match whose own edge characters are not word characters', () => {
expect(isWordBoundaryMatch('a!!!!b', 1, 5)).toBe(true)
})
it('reads an out-of-range probe as a non-word character rather than a match', () => {
expect(isWordBoundaryMatch('abc', 0, 3)).toBe(true)
expect(isWordBoundaryMatch('abc', 3, 3)).toBe(true)
})
})
@@ -0,0 +1,96 @@
/**
* Decides how a known secret literal is allowed to match inside a larger string.
*
* The matcher knows every secret's exact bytes, so this is not detection — it is the narrower
* question of whether a substring hit is distinctive enough to be attributed to the secret rather
* than to coincidence. A four-character value such as `test` occurs inside ordinary words; an
* eight-character one effectively does not.
*/
/**
* `'anywhere'` substitutes a hit at any offset.
*
* `'boundary'` substitutes a hit only when it sits on a word boundary, so a short literal can still
* be replaced when it stands alone (`test`), is delimited (`key=test`, `"test"`, `user_test`), or is
* the whole value, but cannot rewrite the interior of an unrelated token (`latest`).
*/
export type ResolvedSecretMatchPolicy = 'anywhere' | 'boundary'
/**
* Shortest literal that may be substituted at an arbitrary offset inside surrounding text.
*
* Length, not randomness, is what makes a coincidental hit implausible. Shannon entropy measured
* over a literal's own character distribution answers "is this string internally varied", which is
* not the same question and misfires badly on real credentials: an all-`f` 32-character HMAC key
* scores 0.00 bits/char, a zero-padded card number scores 0.34, a zero-padded AWS key id scores
* 1.02, and a zero-padded `sk_live_` key scores 1.50 — every one of them a full-length secret that
* an entropy floor would demote. Sampling confirms the same for genuinely random values, where the
* finite-sample bias of a short string drags the estimate down: at a 3.0 bits/char floor, 46% of
* random 12-character hex, 74% of random 16-digit numerics, and 99% of 9-digit values fall below it.
*
* Eight is chosen because every false positive observed in practice came from a value of seven
* characters or fewer, and because a literal that short is the only kind that plausibly appears
* inside unrelated log text by accident. Values below the floor are still substituted — they just
* have to land on a word boundary, which covers standing alone, delimited, and whole-value cases.
*/
export const MIN_UNANCHORED_MATCH_LENGTH = 8
/**
* Combining marks count so a substitution cannot split a grapheme cluster. `_` deliberately does
* NOT: `sk_live_...` and `user_483920_profile` are the dominant way a secret gets joined into an
* identifier, and treating `_` as a word character would suppress those hits entirely.
*/
const WORD_CHARACTER = /[\p{L}\p{N}\p{M}]/u
/** Classifies one secret literal by whether a hit on it could plausibly be a coincidence. */
export function getResolvedSecretMatchPolicy(plaintext: string): ResolvedSecretMatchPolicy {
return plaintext.length >= MIN_UNANCHORED_MATCH_LENGTH ? 'anywhere' : 'boundary'
}
/**
* Reads the whole code point occupying `index`, including when `index` addresses the trailing half
* of a surrogate pair. Returns undefined out of range, which callers treat as "not a word
* character" so an out-of-bounds probe widens the match rather than suppressing it.
*/
function codePointAt(value: string, index: number): number | undefined {
if (index < 0 || index >= value.length) return undefined
const code = value.codePointAt(index)
if (code !== undefined && code >= 0xdc00 && code <= 0xdfff && index > 0) {
const paired = value.codePointAt(index - 1)
if (paired !== undefined && paired > 0xffff) return paired
}
return code
}
function isWordCharacter(value: string, index: number): boolean {
const code = codePointAt(value, index)
if (code === undefined) return false
if (code < 0x80) {
return (code >= 48 && code <= 57) || (code >= 65 && code <= 90) || (code >= 97 && code <= 122)
}
return WORD_CHARACTER.test(String.fromCodePoint(code))
}
/**
* True when the span `[start, end)` is not spliced into the middle of a surrounding word.
*
* A boundary exists wherever two adjacent characters are not both word characters, which is the
* generalization of a regex `\b` to a span. `key=test` and `"test"` are anchored because `=` and
* `"` are not word characters; `latest` is not, because `a` and `t` both are. A whole-value match
* is anchored by the string edges, so an exact value is always replaceable regardless of policy.
*/
export function isWordBoundaryMatch(value: string, start: number, end: number): boolean {
const startsWord = isWordCharacter(value, start - 1) && isWordCharacter(value, start)
const endsWord = isWordCharacter(value, end) && isWordCharacter(value, end - 1)
return !startsWord && !endsWord
}
/** True when a hit at `[start, end)` may be substituted under `policy`. Omitted policy is wide. */
export function satisfiesResolvedSecretMatchPolicy(
value: string,
start: number,
end: number,
policy: ResolvedSecretMatchPolicy | undefined
): boolean {
return policy !== 'boundary' || isWordBoundaryMatch(value, start, end)
}
@@ -3,9 +3,12 @@
*/
import { describe, expect, it } from 'vitest'
import {
type CreateResolvedSecretMatcherOptions,
containsResolvedSecret,
createResolvedSecretMatcher,
OPAQUE_RESOLVED_SECRET_REPLACEMENT,
type ResolvedSecretMatch,
type ResolvedSecretMatcher,
sanitizeResolvedSecretPrimitive,
sanitizeResolvedSecretString,
scanResolvedSecretString,
@@ -192,3 +195,176 @@ describe('resolved secret matcher', () => {
expect(sanitizeResolvedSecretString('Test', matcher)).toBe('')
})
})
describe('resolved secret matcher match policy', () => {
const SHORT = [{ plaintext: 'test', replacement: '{{TOKEN}}' }]
const API_KEY = 'sk-proj-Ab3xK9mQ2pLw7nRt5vYc8Zd4'
const LONG = [{ plaintext: API_KEY, replacement: '{{API_KEY}}' }]
function build(
matches: ResolvedSecretMatch[],
options?: CreateResolvedSecretMatcherOptions
): ResolvedSecretMatcher {
const matcher = createResolvedSecretMatcher(matches, options)
if (!matcher) throw new Error('expected a matcher')
return matcher
}
it('matches a short literal anywhere when classifying content', () => {
const matcher = build(SHORT)
expect(containsResolvedSecret('the latest news', matcher)).toBe(true)
expect(sanitizeResolvedSecretString('the latest news', matcher)).toBe('the la{{TOKEN}} news')
})
it.each([
['test', '{{TOKEN}}'],
['key=test', 'key={{TOKEN}}'],
['"test"', '"{{TOKEN}}"'],
['{"k":"test"}', '{"k":"{{TOKEN}}"}'],
['test test', '{{TOKEN}} {{TOKEN}}'],
['user_test_id', 'user_{{TOKEN}}_id'],
])('still renders a boundary-anchored short literal in %s', (value, expected) => {
const matcher = build(SHORT, { mode: 'render' })
expect(sanitizeResolvedSecretString(value, matcher)).toBe(expected)
expect(containsResolvedSecret(value, matcher)).toBe(true)
})
it.each(['the latest news', 'tested', 'prefixtest'])(
'leaves an unanchored short literal in %s untouched when rendering',
(value) => {
const matcher = build(SHORT, { mode: 'render' })
expect(sanitizeResolvedSecretString(value, matcher)).toBe(value)
expect(containsResolvedSecret(value, matcher)).toBe(false)
}
)
it('renders a full-length literal at any offset, including mid-token', () => {
const matcher = build(LONG, { mode: 'render' })
expect(sanitizeResolvedSecretString(`prefix${API_KEY}suffix`, matcher)).toBe(
'prefix{{API_KEY}}suffix'
)
expect(containsResolvedSecret(`prefix${API_KEY}suffix`, matcher)).toBe(true)
})
/** Prefixed shapes are assembled at runtime so no source literal reads as a live credential. */
it.each([
['f'.repeat(32), 'all-f HMAC key'],
['4111111111111111', 'test PAN'],
[`AKIA${'0'.repeat(16)}`, 'padded AWS key id'],
[`sk_live_${'0'.repeat(24)}`, 'padded stripe-style key'],
])('renders low-variety full-length credential (%s) mid-token', (secret) => {
const matcher = build([{ plaintext: secret, replacement: '{{KEY}}' }], { mode: 'render' })
expect(sanitizeResolvedSecretString(`etag_${secret}x`, matcher)).toBe('etag_{{KEY}}x')
expect(containsResolvedSecret(`etag_${secret}x`, matcher)).toBe(true)
})
it('settles a boundary that an earlier substitution exposed', () => {
const matcher = build(
[
{ plaintext: API_KEY, replacement: '{{API_KEY}}' },
{ plaintext: 'test', replacement: '{{TOKEN}}' },
],
{ mode: 'render' }
)
expect(sanitizeResolvedSecretString(`${API_KEY}test`, matcher)).toBe('{{API_KEY}}{{TOKEN}}')
})
it('settles a literal that an empty replacement spliced into existence', () => {
const matcher = build([
{ plaintext: API_KEY, replacement: '' },
{ plaintext: 'password', replacement: '{{PW}}' },
])
expect(sanitizeResolvedSecretString(`pass${API_KEY}word`, matcher)).toBe('{{PW}}')
})
it('keeps the substitution pass and its invariant in agreement', () => {
const matcher = build(SHORT, { mode: 'render' })
for (const value of ['the latest news', 'key=test', 'contest testable test']) {
const sanitized = sanitizeResolvedSecretString(value, matcher)
expect(containsResolvedSecret(sanitized, matcher)).toBe(false)
}
})
it('reports a suppressed match to provenance callbacks so detection stays conservative', () => {
const matcher = build(SHORT, { mode: 'render' })
const matches: string[] = []
expect(
sanitizeResolvedSecretString('the latest news', matcher, undefined, (plaintext) =>
matches.push(plaintext)
)
).toBe('the latest news')
expect(matches).toEqual(['test'])
})
it.each([
['Test', '{{Test}}'],
['{{Test}}', '{{Test}}'],
['Test {{Test}} Test', '{{Test}} {{Test}} {{Test}}'],
['laTest news', 'laTest news'],
])('preserves named provenance labels under the render policy for %s', (value, expected) => {
const matcher = build([{ plaintext: 'Test', replacement: '{{Test}}' }], {
...PRESERVE_NAMED_PROVENANCE,
mode: 'render',
})
expect(sanitizeResolvedSecretString(value, matcher)).toBe(expected)
})
it('keeps the protected-placeholder behaviours under the options production uses', () => {
const composite = build(
[
{ plaintext: 'x{{Test}}y', replacement: '{{COMPOSITE}}' },
{ plaintext: 'Test', replacement: '{{Test}}' },
],
{ ...PRESERVE_NAMED_PROVENANCE, mode: 'render' }
)
expect(sanitizeResolvedSecretString('x{{Test}}y', composite)).toBe('{{COMPOSITE}}')
const malformed = build([{ plaintext: 'Test', replacement: '{{Test{B}}}' }], {
...PRESERVE_NAMED_PROVENANCE,
mode: 'render',
})
expect(sanitizeResolvedSecretString('Test', malformed)).toBe(OPAQUE_RESOLVED_SECRET_REPLACEMENT)
const chained = build(
[
{ plaintext: 'Test', replacement: 'visible-Test' },
{ plaintext: 'REDACTED', replacement: '{{OTHER}}' },
],
{ mode: 'render' }
)
expect(sanitizeResolvedSecretString('Test', chained)).toBe('')
})
it('keeps exact replacement available below the length floor', () => {
const matcher = build([{ plaintext: '23', replacement: '{{TOKEN}}' }], { mode: 'render' })
expect(sanitizeResolvedSecretPrimitive('23', matcher)).toBe('{{TOKEN}}')
expect(sanitizeResolvedSecretString('23', matcher)).toBe('{{TOKEN}}')
expect(sanitizeResolvedSecretString('123', matcher)).toBe('123')
})
it('builds a matcher for an astral-plane literal instead of failing construction', () => {
const secret = 'k\u{1F600}ey12345'
const matcher = build([{ plaintext: secret, replacement: '{{EMOJI}}' }], { mode: 'render' })
expect(sanitizeResolvedSecretString(`token ${secret} end`, matcher)).toBe('token {{EMOJI}} end')
})
it('does not rewrite a token interior next to an astral-plane letter', () => {
const matcher = build(SHORT, { mode: 'render' })
expect(sanitizeResolvedSecretString('\u{1D400}test\u{1D401}', matcher)).toBe(
'\u{1D400}test\u{1D401}'
)
})
})
@@ -1,13 +1,23 @@
import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limits'
import {
getResolvedSecretMatchPolicy,
type ResolvedSecretMatchPolicy,
satisfiesResolvedSecretMatchPolicy,
} from '@/executor/utils/resolved-secret-match-policy'
import { getResolvedSecretMatcherCapacityFailure } from '@/executor/utils/resolved-secret-matcher-capacity'
const MAX_MATCH_EVENTS = 1_000_000
/** Bounds the substitute/verify loop in {@link sanitizeResolvedSecretString}. */
const MAX_SETTLE_PASSES = 4
export const OPAQUE_RESOLVED_SECRET_REPLACEMENT = '[REDACTED_SECRET]'
interface SecretReplacement {
plaintext: string
replacement: string
/** Absent on a detect matcher, where every literal matches at any offset. */
policy?: ResolvedSecretMatchPolicy
}
interface SecretTrieNode {
@@ -36,6 +46,19 @@ export interface CreateResolvedSecretMatcherOptions {
* the exact plaintext that produced it; overlapping secret literals remain detectable.
*/
preserveNamedProvenanceLabels?: boolean
/**
* `'detect'` (the default) matches every literal at any offset. Use it wherever a hit only
* classifies content — provenance export, file-safety scans — because there a coincidental hit
* costs an over-broad label while a missed hit can wrongly certify content as secret-free.
*
* `'render'` restricts literals below {@link MIN_UNANCHORED_MATCH_LENGTH} to word-boundary hits.
* Use it wherever a hit rewrites text. A projection's own post-check must be built with the same
* options as the projection it verifies: it asks "did I substitute what I promised", so reading a
* wider match set would make it demand replacements the projector deliberately declined and drop
* the content instead. That does mean such a check cannot see a short literal sitting inside an
* unrelated token — that occurrence is defined as coincidental here, not overlooked.
*/
mode?: 'detect' | 'render'
}
class ResolvedSecretMatcherError extends Error {
@@ -102,6 +125,17 @@ function createMatcherFromReplacements(
}
}
/** Walks by code unit, matching how {@link createMatcherFromReplacements} keys the trie. */
function findTerminalNode(root: SecretTrieNode, plaintext: string): SecretTrieNode {
let node = root
for (let index = 0; index < plaintext.length; index += 1) {
const child = node.children.get(plaintext[index])
if (!child) throw new ResolvedSecretMatcherError('Secret matcher construction failed')
node = child
}
return node
}
function advanceMatcher(
matcher: ResolvedSecretMatcher,
node: SecretTrieNode,
@@ -208,7 +242,8 @@ export function containsResolvedSecret(value: string, matcher: ResolvedSecretMat
protectedSpan?.start ?? -1,
protectedSpan?.end ?? -1,
protectedSpan?.plaintexts
)
) &&
satisfiesResolvedSecretMatchPolicy(value, start, end, outputNode.replacement.policy)
) {
return true
}
@@ -284,6 +319,30 @@ export function sanitizeResolvedSecretString(
matcher: ResolvedSecretMatcher,
maxBytes = MAX_INLINE_MATERIALIZATION_BYTES,
onMatch?: (plaintext: string) => void
): string {
/**
* A substitution can leave a literal the previous pass could not act on: it may expose a word
* boundary that suppressed a narrow-policy match (`<key>test` becoming `{{KEY}}test`), and an
* empty replacement can splice its neighbours into a literal that was not present in the input.
* Each pass strictly consumes matches, so this converges in practice; the bound is what keeps a
* pathological chain from looping, and the throw past it stays the fail-closed backstop callers
* already handle by dropping the value.
*/
let sanitized = substituteResolvedSecrets(value, matcher, maxBytes, onMatch)
for (let pass = 1; containsResolvedSecret(sanitized, matcher); pass += 1) {
if (pass >= MAX_SETTLE_PASSES) {
throw new ResolvedSecretMatcherError('Sanitized content still contains an active secret')
}
sanitized = substituteResolvedSecrets(sanitized, matcher, maxBytes, onMatch)
}
return sanitized
}
function substituteResolvedSecrets(
value: string,
matcher: ResolvedSecretMatcher,
maxBytes: number,
onMatch?: (plaintext: string) => void
): string {
if (maxBytes < 0) {
throw new ResolvedSecretMatcherError('Sanitized secret-bearing string exceeds the size limit')
@@ -359,6 +418,7 @@ export function sanitizeResolvedSecretString(
protectedSpan?.end ?? -1,
protectedSpan?.plaintexts
) &&
satisfiesResolvedSecretMatchPolicy(value, start, end, outputNode.replacement.policy) &&
start >= emitCursor
) {
const slot = start % windowSize
@@ -375,11 +435,7 @@ export function sanitizeResolvedSecretString(
finalizeThrough(value.length - 1)
append(value.slice(literalStart))
const sanitized = chunks.join('')
if (containsResolvedSecret(sanitized, matcher)) {
throw new ResolvedSecretMatcherError('Sanitized content still contains an active secret')
}
return sanitized
return chunks.join('')
}
/** Replaces only an exact primitive rendering, never a substring of another primitive. */
@@ -454,13 +510,9 @@ export function createResolvedSecretMatcher(
const exactReplacements = new Map<string, string>()
const protectedReplacementPlaintexts = new Map<string, ReadonlySet<string>>()
const assigned: SecretReplacement[] = []
for (const { plaintext, replacement } of provisional) {
let node = detector.root
for (const character of plaintext) {
const child = node.children.get(character)
if (!child) throw new ResolvedSecretMatcherError('Secret matcher construction failed')
node = child
}
const node = findTerminalNode(detector.root, plaintext)
const namedReplacement = isNamedResolvedSecretReplacement(replacement)
const replacementContainsSecret = containsResolvedSecret(replacement, detector)
const safeReplacement = !replacementContainsSecret
@@ -476,8 +528,20 @@ export function createResolvedSecretMatcher(
plaintext,
replacement: safeReplacement,
}
assigned.push(node.replacement)
exactReplacements.set(plaintext, safeReplacement)
}
/**
* Applied only after every construction-time safety check above has run, so those checks always
* see the widest match set and a narrow policy can never talk one of them out of failing closed.
*/
if (options.mode === 'render') {
for (const replacement of assigned) {
replacement.policy = getResolvedSecretMatchPolicy(replacement.plaintext)
}
}
detector.exactReplacements = exactReplacements
detector.protectedReplacementPlaintexts = protectedReplacementPlaintexts
detector.protectedReplacementMatcher = createProtectedReplacementMatcher(
@@ -135,18 +135,51 @@ describe('projectToolResultForCopilot', () => {
})
it('uses an opaque marker when a replacement contains another active literal', () => {
const middle = 'Kq7Xz2Lm9P'
const registry = new ResolvedSecretTraceRegistry([
{ name: 'MIDDLE', plaintext: 'B', encryptedValue: 'encrypted-b' },
{ name: 'MIDDLE', plaintext: middle, encryptedValue: 'encrypted-middle' },
{ name: 'BRACE', plaintext: '{', encryptedValue: 'encrypted-brace' },
{ name: 'JOINED', plaintext: 'ac', encryptedValue: 'encrypted-ac' },
])
registry.recordResolved('MIDDLE', 'B', { propagated: true })
registry.recordResolved('MIDDLE', middle, { propagated: true })
registry.recordResolved('BRACE', '{', { propagated: true })
registry.recordResolved('JOINED', 'ac', { propagated: true })
expect(projectToolResultForCopilot({ success: true, output: 'aBc' }, registry)).toEqual({
expect(projectToolResultForCopilot({ success: true, output: `a${middle}c` }, registry)).toEqual(
{
success: true,
output: 'a[REDACTED_SECRET]c',
}
)
})
it('projects a short secret standing alone or delimited, but not inside another word', () => {
const registry = new ResolvedSecretTraceRegistry([
{ name: 'PIN', plaintext: '483920', encryptedValue: 'encrypted-pin' },
])
registry.recordResolved('PIN', '483920', { propagated: true })
expect(
projectToolResultForCopilot(
{
success: true,
output: {
whole: '483920',
delimited: 'code=483920',
underscored: 'user_483920_profile',
embedded: 'ref483920x',
},
},
registry
)
).toEqual({
success: true,
output: 'a[REDACTED_SECRET]c',
output: {
whole: '{{PIN}}',
delimited: 'code={{PIN}}',
underscored: 'user_{{PIN}}_profile',
embedded: 'ref483920x',
},
})
})
@@ -744,12 +744,12 @@ describe('LoggingSession completion retries', () => {
status: 'success',
output: { apiKey: 'ordinary-value' },
displayResolvedSecretTraceProvenance: createDisplayProvenance([
{ plaintext: 'E', replacement: '{{X}}' },
{ plaintext: 'REDACTED', replacement: '{{X}}' },
]),
},
]
session.setResolvedSecretTraceRegistry(
createSecretRegistry([{ plaintext: 'E', replacement: '{{X}}' }])
createSecretRegistry([{ plaintext: 'REDACTED', replacement: '{{X}}' }])
)
prepareTraceSpansForProjectionMock.mockImplementationOnce(
async ({ traceSpans }: { traceSpans: Array<Record<string, unknown>> }) =>
@@ -442,7 +442,7 @@ describe('projectTraceSpansForSecrets', () => {
const source = [createSpan({ output: { apiKey: '[REDACTED]' } })]
const result = await enforceTraceSpanSecretInvariant(source, {
registry: createRegistry([{ plaintext: 'E', replacement: '{{X}}' }]),
registry: createRegistry([{ plaintext: 'REDACTED', replacement: '{{X}}' }]),
store: STORE,
})
@@ -450,11 +450,23 @@ describe('projectTraceSpansForSecrets', () => {
expect(source[0].output).toEqual({ apiKey: '[REDACTED]' })
})
it('keeps content whose only literal occurrence sits inside an unrelated word', async () => {
const source = [createSpan({ output: { summary: 'the latest news' } })]
const result = await enforceTraceSpanSecretInvariant(source, {
registry: createRegistry([{ plaintext: 'test', replacement: '{{TOKEN}}' }]),
store: STORE,
})
expect(result).toBe(source)
expect(result[0].output).toEqual({ summary: 'the latest news' })
})
it('names the invariant that forced the structural fallback', async () => {
const source = [createSpan({ output: { apiKey: '[REDACTED]' } })]
await enforceTraceSpanSecretInvariant(source, {
registry: createRegistry([{ plaintext: 'E', replacement: '{{X}}' }]),
registry: createRegistry([{ plaintext: 'REDACTED', replacement: '{{X}}' }]),
store: STORE,
})
@@ -520,7 +532,7 @@ describe('projectTraceSpansForSecrets', () => {
materializeLargeValueRefMock.mockResolvedValue({ value: '{{X}}' })
const result = await enforceTraceSpanSecretInvariant(source, {
registry: createRegistry([{ plaintext: 'E', replacement: '{{X}}' }]),
registry: createRegistry([{ plaintext: 'REDACTED', replacement: '{{X}}' }]),
store: STORE,
})
@@ -971,7 +983,8 @@ describe('projectTraceSpansForSecrets', () => {
kind: 'array' as const,
size: 1,
}))
const expandedValue = 'a'.repeat(1024)
const expandingSecret = 'a7Kq2Xz9Lm4P'
const expandedValue = expandingSecret.repeat(1024)
materializeLargeValueRefMock.mockImplementation(async () => [expandedValue])
storeLargeValueMock.mockImplementation(
async (_value: unknown, _json: string, size: number) => ({
@@ -996,7 +1009,7 @@ describe('projectTraceSpansForSecrets', () => {
const result = await projectTraceSpansForSecrets(
[createSpan({ input: { ok: true }, output: { items: manifest } })],
{
registry: createRegistry([{ plaintext: 'a', replacement: 'X'.repeat(1024) }]),
registry: createRegistry([{ plaintext: expandingSecret, replacement: 'X'.repeat(1024) }]),
store: STORE,
}
)
@@ -1537,6 +1537,7 @@ export async function enforceTraceSpanSecretInvariant(
const matcher = createResolvedSecretMatcher(options.registry.getActiveMatches(), {
preserveNamedProvenanceLabels: true,
mode: 'render',
})
if (!matcher) return traceSpans
@@ -1565,6 +1566,7 @@ export async function projectTraceSpansForSecrets(
try {
const matcher = createResolvedSecretMatcher(options.registry.getActiveMatches(), {
preserveNamedProvenanceLabels: true,
mode: 'render',
})
if (!matcher) return cloneTraceSpansForProjection(traceSpans)