diff --git a/scripts/fba-core-regression-test-env.mjs b/scripts/fba-core-regression-test-env.mjs index 1f6229a6f..621416ae0 100644 --- a/scripts/fba-core-regression-test-env.mjs +++ b/scripts/fba-core-regression-test-env.mjs @@ -68,12 +68,17 @@ export function validateCoreEnvironment(env,root=repositoryRoot) { return base.origin } export function redactEvidence(value,secrets=[]) { - if(Array.isArray(value))return value.map(item=>redactEvidence(item,secrets)) - if(value&&typeof value==='object')return Object.fromEntries(Object.entries(value).map(([key,item])=>[key, - /^(?:.*password|.*token|authorization|cookie|gateway|jenkinsUrl|privateKey)$/i.test(key)?'[REDACTED]':redactEvidence(item,secrets)])) - if(typeof value==='string')return secrets.filter(secret=>typeof secret==='string'&&secret.length>0) - .reduce((text,secret)=>text.split(secret).join('[REDACTED]'),value) - return value + // Long runs can register the same credential repeatedly. Normalize once per + // report, rather than filtering and reapplying it for every nested string. + const uniqueSecrets=[...new Set(secrets)].filter(secret=>typeof secret==='string'&&secret.length>0) + function redact(item) { + if(Array.isArray(item))return item.map(redact) + if(item&&typeof item==='object')return Object.fromEntries(Object.entries(item).map(([key,child])=>[key, + /^(?:.*password|.*token|authorization|cookie|gateway|jenkinsUrl|privateKey)$/i.test(key)?'[REDACTED]':redact(child)])) + if(typeof item==='string')return uniqueSecrets.reduce((text,secret)=>text.split(secret).join('[REDACTED]'),item) + return item + } + return redact(value) } export function modePassed(report) { const proof=report?.createdResources?.phase5RootFulfillment diff --git a/scripts/fba-core-regression-test-env.test.mjs b/scripts/fba-core-regression-test-env.test.mjs index ace7bca1d..3e8d11926 100644 --- a/scripts/fba-core-regression-test-env.test.mjs +++ b/scripts/fba-core-regression-test-env.test.mjs @@ -124,6 +124,17 @@ test('preflight failure creates summary but no HTTP fixture; existing run or act await assert.rejects(runFbaCore({...config(),runId:'next',outputDir:'reports/next'},{env:env(),outputRoot,readers:readers()})) assert.equal(readFileSync(lock,'utf8'),'occupied') }) +test('redaction scans the secret input once for the complete report tree',()=>{ + let reads=0 + const values=Array.from({length:80},()=> 'synthetic-private-value') + const secrets=new Proxy(values,{get(target,key,receiver){if(/^\d+$/.test(String(key)))reads++;return Reflect.get(target,key,receiver)}}) + const report={steps:Array.from({length:40},()=>({message:'prefix synthetic-private-value suffix',nested:{token:'hidden',public:'kept'}}))} + const result=redactEvidence(report,secrets) + assert.ok(result.steps.every(row=>row.message==='prefix [REDACTED] suffix'&&row.nested.token==='[REDACTED]'&&row.nested.public==='kept')) + assert.equal(reads,values.length,'normalization must not repeat for every evidence string') + assert.equal(report.steps[0].message,'prefix synthetic-private-value suffix','input report is not modified') +}) + test('reports redact dynamically created account secrets and environment/token values',()=>{ const value=redactEvidence({password:'x',pdaToken:'y',message:'failure private-secret',nested:{gateway:'url',value:'safe'}},['private-secret']) assert.equal(value.password,'[REDACTED]');assert.equal(value.pdaToken,'[REDACTED]')