// The privacy scan of signed macOS artifacts leaves out Apple code signatures only (tools/mac-signature-scan.ts): // a term inside a signature passes, the same term anywhere else (a resource, the binary's own bytes) fails. import { afterAll, describe, expect, test } from 'bun:test'; import { spawnSync } from 'node:child_process'; import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { blankDmgSignature, dmgSignatureRange, isMachO, scanSignedMac } from '../tools/mac-signature-scan'; import { type Report, scanPath, type Term } from '../tools/privacy-scan'; const TERM = 'Qzvfakecorp'; const terms: Term[] = [{ text: TERM, short: false }]; const mac = process.platform === 'darwin'; const dir = mkdtempSync(join(tmpdir(), 'sb-macsig-test-')); afterAll(() => rmSync(dir, { recursive: true, force: true })); const empty = (): Report => ({ files: 0, bytes: 0, hits: [], errors: [] }); const ok = (cmd: string[]) => { const r = spawnSync(cmd[0]!, cmd.slice(1), { encoding: 'utf8' }); if (r.status !== 0) throw new Error(`${cmd.join(' ')}: ${r.stderr}`); }; /** A fake app whose executable is signed with the term in its signature (the identifier lives in the CodeDirectory). */ function fakeApp(name: string, resource?: string): string { const app = join(dir, name, 'Fake.app'); mkdirSync(join(app, 'Contents', 'MacOS'), { recursive: true }); mkdirSync(join(app, 'Contents', 'Resources'), { recursive: true }); copyFileSync('/usr/bin/true', join(app, 'Contents', 'MacOS', 'fake')); ok(['codesign', '-f', '-s', '-', '-i', `com.${TERM}.fake`, join(app, 'Contents', 'MacOS', 'fake')]); writeFileSync(join(app, 'Contents', 'Resources', 'about.txt'), resource ?? 'nothing private here\n'); return app; } describe.skipIf(!mac)('signed macOS artifacts: signatures left out of the privacy scan', () => { test('the term only in a code signature: the plain scan finds it, the signed-mac scan passes', () => { const app = fakeApp('sig-only'); const bin = join(app, 'Contents', 'MacOS', 'fake'); expect(isMachO(readFileSync(bin).subarray(0, 4))).toBe(true); const plain = empty(); scanPath(app, terms, 'Fake.app', plain); expect(plain.hits.length).toBeGreaterThan(0); const r = scanSignedMac(app, terms, empty()); expect(r.errors).toEqual([]); expect(r.hits).toEqual([]); // the original stays signed (only a copy is stripped) ok(['codesign', '--verify', bin]); }); test('the same term in a resource file still fails', () => { const app = fakeApp('in-resource', `made by ${TERM}\n`); const r = scanSignedMac(app, terms, empty()); expect(r.hits.map(h => h.file)).toEqual(['Fake.app/Contents/Resources/about.txt']); }); test('updater .app.tar.gz: the unpacked app is stripped, a resource hit fails', () => { const good = fakeApp('tar-good'), bad = fakeApp('tar-bad', `${TERM}\n`); const tg = join(dir, 'good.app.tar.gz'), tb = join(dir, 'bad.app.tar.gz'); ok(['tar', '-czf', tg, '-C', join(good, '..'), 'Fake.app']); ok(['tar', '-czf', tb, '-C', join(bad, '..'), 'Fake.app']); expect(scanSignedMac(tg, terms, empty()).hits).toEqual([]); expect(scanSignedMac(tb, terms, empty()).hits.map(h => h.file)).toEqual(['bad.app.tar.gz!/Fake.app/Contents/Resources/about.txt']); }); test('DMG: its own signature blob is zeroed, the term in the volume fails', () => { const src = join(dir, 'dmg-src'); mkdirSync(src, { recursive: true }); writeFileSync(join(src, 'readme.txt'), 'hello\n'); const dmg = join(dir, 'x.dmg'); ok(['hdiutil', 'create', '-quiet', '-fs', 'HFS+', '-srcfolder', src, '-format', 'UDZO', '-ov', dmg]); ok(['codesign', '-f', '-s', '-', '-i', `com.${TERM}.dmg`, dmg]); const data = readFileSync(dmg); const range = dmgSignatureRange(data); expect(range).not.toBeNull(); expect(data.subarray(range!.offset, range!.offset + range!.length).includes(Buffer.from(TERM))).toBe(true); expect(Buffer.from(blankDmgSignature(data)).includes(Buffer.from(TERM))).toBe(false); expect(scanSignedMac(dmg, terms, empty()).hits).toEqual([]); writeFileSync(join(src, 'readme.txt'), `by ${TERM}\n`); const dmg2 = join(dir, 'y.dmg'); ok(['hdiutil', 'create', '-quiet', '-fs', 'HFS+', '-srcfolder', src, '-format', 'UDZO', '-ov', dmg2]); expect(scanSignedMac(dmg2, terms, empty()).hits.map(h => h.file)).toEqual(['y.dmg!/readme.txt']); }, 60_000); });