diff --git a/lib/cso/process.ts b/lib/cso/process.ts index de25d403c..134484e23 100644 --- a/lib/cso/process.ts +++ b/lib/cso/process.ts @@ -205,8 +205,10 @@ export async function git(repo: string, args: string[], home: string): PromiseknownOperations.has(value))??'metadata', + phase=operation==='rev-parse'&&args.includes('--show-object-format')?'object-format':operation==='rev-parse'&&args.includes('--is-inside-work-tree')?'worktree-probe':operation, + reason=/not a git repository|outside repository/i.test(result.stderr)?'repository unavailable':/dubious ownership/i.test(result.stderr)?'repository ownership rejected':/(?:bad|invalid|unable to read).*config|config (?:error|file)/i.test(result.stderr)?'configuration rejected':/unknown option|unknown switch|unrecognized option|usage:/i.test(result.stderr)?'unsupported invocation':/(?:cannot|could not|unable to) (?:chdir|change directory)|no such file or directory/i.test(result.stderr)?'path unavailable':'request rejected', outcome=result.timedOut?'timed out':result.truncated?'exceeded the output limit':`exited ${result.code}`; - throw new CsoError('MISSING_INPUT',`Could not read bounded Git metadata: ${operation} ${outcome}; source may not be a Git repository`); + throw new CsoError('MISSING_INPUT',`Could not read bounded Git metadata: ${phase} ${outcome} (${reason}); source may not be a Git repository`); } return result.stdout; } diff --git a/test/cso-git-hardening.test.ts b/test/cso-git-hardening.test.ts index 971ce7f6b..350f2f929 100644 --- a/test/cso-git-hardening.test.ts +++ b/test/cso-git-hardening.test.ts @@ -36,10 +36,20 @@ describe('CSO Git metadata hardening',()=>{ try{await readGitMetadata(repo,['rev-parse','--verify','secret-ref-name'],root);}catch(error){failure=error;} expect(failure).toMatchObject({code:'MISSING_INPUT'}); expect(failure.message).toContain('rev-parse exited'); + expect(failure.message).toContain('(request rejected)'); expect(failure.message).not.toContain(root); expect(failure.message).not.toContain('secret-ref-name'); }); + test.skipIf(process.platform==='win32')('labels the fixed object-format phase without exposing repository data',async()=>{ + const {root,repo}=fixture();fs.appendFileSync(path.join(repo,'.git','config'),'\n[broken configuration\n');let failure:any; + try{await readGitMetadata(repo,['rev-parse','--show-object-format'],root);}catch(error){failure=error;} + expect(failure).toMatchObject({code:'MISSING_INPUT'}); + expect(failure.message).toContain('object-format exited'); + expect(failure.message).toContain('(configuration rejected)'); + expect(failure.message).not.toContain(root); + }); + test.skipIf(process.platform==='win32')('refuses trusted Git calls that are not bound to one audited worktree',async()=>{ const {root}=fixture(); await expect(runProcess(executable('git'),['rev-parse','--is-inside-work-tree'],{cwd:root,env:childEnvironment(root),raw:true})).rejects.toMatchObject({code:'INVALID_ARGUMENT'}); diff --git a/test/cso-snapshot-disappearance-race.test.ts b/test/cso-snapshot-disappearance-race.test.ts index 7b9486634..4896fb0a6 100644 --- a/test/cso-snapshot-disappearance-race.test.ts +++ b/test/cso-snapshot-disappearance-race.test.ts @@ -16,11 +16,15 @@ function trustedGit():string{ function gitEnvironment(home:string):NodeJS.ProcessEnv{ return{...process.env,HOME:home,GIT_CONFIG_NOSYSTEM:'1',GIT_CONFIG_GLOBAL:process.platform==='win32'?'NUL':'/dev/null',GIT_TERMINAL_PROMPT:'0'}; } -type FileIdentity={dev:bigint;ino:bigint}; -function fileIdentity(file:string):FileIdentity{const stat=fs.statSync(file,{bigint:true});return{dev:stat.dev,ino:stat.ino};} +type FileIdentity={path:string;dev:bigint;ino:bigint}; +function canonicalFile(file:string):string{ + const resolved=fs.realpathSync.native(file); + return process.platform==='win32'?resolved.replace(/^\\\\\?\\UNC\\/i,'\\\\').replace(/^\\\\\?\\/,'').toLowerCase():resolved; +} +function fileIdentity(file:string):FileIdentity{const stat=fs.statSync(file,{bigint:true});return{path:canonicalFile(file),dev:stat.dev,ino:stat.ino};} function isFileIdentity(candidate:unknown,expected:FileIdentity):boolean{ if(path.basename(String(candidate)).toLowerCase()!=='tracked.txt')return false; - try{const current=fs.statSync(String(candidate),{bigint:true});return current.isFile()&¤t.dev===expected.dev&¤t.ino===expected.ino;}catch{return false;} + try{const file=String(candidate),current=fs.statSync(file,{bigint:true});return canonicalFile(file)===expected.path&¤t.dev===expected.dev&¤t.ino===expected.ino;}catch{return false;} } function fixture(){