@@ -497,6 +497,89 @@ describe('file provenance at the actual CLI and model-result boundary', () => {
497497 )
498498 } )
499499
500+ describe ( 'grep reshapes a file line only after redacting it' , ( ) => {
501+ const secret = 'sk_live_P3_FIXTURE_0123456789abcdefghij'
502+ const otherSecret = 'pk_test_P3_FIXTURE_ZYXWVUTSRQPONMLKJIH'
503+ const keyBlock = [
504+ '-----BEGIN FIXTURE KEY-----' ,
505+ 'MIIBFIXTUREKEYLINEONE0123456789' ,
506+ 'MIIBFIXTUREKEYLINETWO0123456789' ,
507+ '-----END FIXTURE KEY-----' ,
508+ ] . join ( '\n' )
509+ const LINE_CHARS = 2_000
510+ const pad = ( from : number , to : number ) => 'x' . repeat ( to - from )
511+
512+ /** A secret leaks as soon as its first or last four characters, or one of its lines, cross. */
513+ function fragments ( value : string ) : string [ ] {
514+ return [ value . slice ( 0 , 4 ) , value . slice ( - 4 ) , ...value . split ( '\n' ) ]
515+ }
516+
517+ async function grepSecretFile ( text : string , pattern : string ) {
518+ const plaintexts : Record < string , string > = {
519+ 'cipher-file' : secret ,
520+ 'cipher-other' : otherSecret ,
521+ 'cipher-key' : keyBlock ,
522+ }
523+ mocks . decrypt . mockImplementation ( async ( cipher : string ) => ( {
524+ decrypted : plaintexts [ cipher ] ,
525+ } ) )
526+ mocks . buffer . mockResolvedValue ( Buffer . from ( text ) )
527+ vi . stubGlobal ( 'fetch' , async ( ) =>
528+ Response . json ( {
529+ data : [ { ...file , size : Buffer . byteLength ( text ) , updatedAt : revision . toISOString ( ) } ] ,
530+ nextCursor : null ,
531+ } )
532+ )
533+ resetDbChainMock ( )
534+ queueTableRows ( workspaceFiles , [
535+ sidecarRow ( 'exact' , revision , [
536+ { name : 'FILE_SECRET' , encryptedValue : 'cipher-file' , sourceUserId : 'reader' } ,
537+ { name : 'OTHER_SECRET' , encryptedValue : 'cipher-other' , sourceUserId : 'reader' } ,
538+ { name : 'KEY_BLOCK' , encryptedValue : 'cipher-key' , sourceUserId : 'reader' } ,
539+ ] ) ,
540+ ] )
541+ const trace = registry ( )
542+ const result = await runCli ( grepFiles ( pattern ) , trace )
543+ const projection = inspectToolResultForCopilot ( result , trace , 'sim_cli' )
544+ expect ( projection . safe ) . toBe ( true )
545+ return JSON . stringify ( projection . result )
546+ }
547+
548+ it . each ( [
549+ [ 'straddling the clip' , `NEEDLE ${ pad ( 7 , LINE_CHARS - 10 ) } ${ secret } tail` ] ,
550+ [
551+ 'whose redaction marker would straddle the clip' ,
552+ `NEEDLE ${ pad ( 7 , LINE_CHARS - 5 ) } ${ secret } ` ,
553+ ] ,
554+ [ 'wholly past the clip' , `NEEDLE ${ pad ( 7 , LINE_CHARS + 50 ) } ${ secret } tail` ] ,
555+ [
556+ 'repeated around the clip with another secret' ,
557+ `NEEDLE ${ otherSecret } ${ pad ( 48 , LINE_CHARS - 12 ) } ${ secret } ${ otherSecret } ${ secret } ` ,
558+ ] ,
559+ ] ) ( 'never returns a fragment of a secret %s' , async ( _case , line ) => {
560+ const projected = await grepSecretFile ( `${ line } \nsecond line` , 'NEEDLE' )
561+ expect ( projected ) . toContain ( 'NEEDLE' )
562+ expect ( projected ) . toContain ( '[line truncated]' )
563+ for ( const fragment of [ ...fragments ( secret ) , ...fragments ( otherSecret ) ] ) {
564+ expect ( projected ) . not . toContain ( fragment )
565+ }
566+ const withoutMarkers = projected
567+ . replaceAll ( '{{FILE_SECRET}}' , '' )
568+ . replaceAll ( '{{OTHER_SECRET}}' , '' )
569+ . replaceAll ( '[REDACTED_SECRET]' , '' )
570+ expect ( withoutMarkers ) . not . toContain ( '{{' )
571+ expect ( withoutMarkers ) . not . toContain ( '[RED' )
572+ } )
573+
574+ it ( 'never returns one line of a multi-line secret' , async ( ) => {
575+ const projected = await grepSecretFile (
576+ `config start\n${ keyBlock } \nconfig end` ,
577+ 'FIXTURE KEY|KEYLINE'
578+ )
579+ for ( const fragment of fragments ( keyBlock ) ) expect ( projected ) . not . toContain ( fragment )
580+ } )
581+ } )
582+
500583 it ( 'refuses an unknown file through the actual CLI text read without loading its bytes' , async ( ) => {
501584 classify ( 'unknown' )
502585 const result = await runCli ( readFile )
0 commit comments