82 lines
5 KiB
TypeScript
82 lines
5 KiB
TypeScript
// tools/privacy-scan.ts: the release gate over every desktop artifact. Made-up terms only (the real ones live in a
|
|
// file outside the repository).
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { collectTerms, kindOf, mask, parseTerms, scan, scanBuffer, squashfsOffset, type Term, UPSTREAM_PATHS } from '../tools/privacy-scan';
|
|
|
|
const T: Term[] = [{ text: 'Quillfeather', short: false }, { text: 'qz', short: true }];
|
|
// home folders are put together at run time, so tools/scrub.ts (which forbids them in the repository) passes
|
|
const home = (root: string, name: string): string => `${root}${name}`;
|
|
|
|
describe('privacy scan', () => {
|
|
test('terms: comments and blanks skipped, never printed in full', () => {
|
|
expect(parseTerms('# names\nQuillfeather\n\n qz \n')).toEqual(['Quillfeather', 'qz']);
|
|
expect(mask('Quillfeather')).toBe('Q***');
|
|
const dir = mkdtempSync(join(tmpdir(), 'sb-terms-'));
|
|
writeFileSync(join(dir, 't.txt'), `Quillfeather\nCRT\n${['hi', 'crt.fyi'].join('@')}\n`);
|
|
// the release identity is never a forbidden term
|
|
expect(collectTerms(join(dir, 't.txt'), false).map(t => t.text)).toEqual(['Quillfeather']);
|
|
});
|
|
|
|
test('finds a term in any case, in UTF-8 and UTF-16LE', () => {
|
|
const hits = scanBuffer('bin', Buffer.concat([Buffer.from('xx QUILLFEATHER yy'), Buffer.from('built by quillFeather', 'utf16le')]), T);
|
|
expect(hits.map(h => h.encoding)).toEqual(['utf8', 'utf16le']);
|
|
expect(hits.every(h => h.what === 'term Q***')).toBe(true);
|
|
expect(scanBuffer('bin', Buffer.from('nothing to see'), T)).toEqual([]);
|
|
});
|
|
|
|
test('a short term counts only in a path', () => {
|
|
expect(scanBuffer('bin', Buffer.from('jazz quiz qz-mode'), T)).toEqual([]);
|
|
expect(scanBuffer('bin', Buffer.from('/opt/qz/src'), T).some(h => h.what === 'term q***')).toBe(true);
|
|
});
|
|
|
|
test('home folders of any account, generic ones allowed', () => {
|
|
const hits = scanBuffer('bin', Buffer.from(`${home('/Users/', 'someone')}/work /home/runner/work ${home('C:\\Users\\', 'other')}\\x /Users/you/app /home/$USER`), []);
|
|
expect(hits.map(h => h.what)).toEqual(['home folder of an account (s***)', 'home folder of an account (o***)']);
|
|
});
|
|
test('upstream GTK icon paths pass, the same account elsewhere does not', () => {
|
|
const gtk = UPSTREAM_PATHS.map(u => `inkscape:export-filename="${u}/x.png"`).join(' ');
|
|
expect(scanBuffer('libgtk-3.so.0', Buffer.from(gtk), [])).toEqual([]);
|
|
const other = UPSTREAM_PATHS[0]!.split('/').slice(0, 3).join('/') + '/secret/x';
|
|
expect(scanBuffer('bin', Buffer.from(other), []).length).toBe(1);
|
|
});
|
|
|
|
test('folders, archives and debug files', () => {
|
|
const dir = mkdtempSync(join(tmpdir(), 'sb-scan-'));
|
|
mkdirSync(join(dir, 'pkg/My.app/Contents'), { recursive: true });
|
|
writeFileSync(join(dir, 'pkg/My.app/Contents/clean.txt'), 'all good');
|
|
writeFileSync(join(dir, 'pkg/My.app/Contents/leak.bin'), Buffer.from(`panic at ${home('/Users/', 'quillfeather')}/src/main.rs`, 'utf8'));
|
|
mkdirSync(join(dir, 'pkg/My.app.dSYM'));
|
|
expect(spawnSync('tar', ['-czf', join(dir, 'bundle.tar.gz'), '-C', join(dir, 'pkg'), '.']).status).toBe(0);
|
|
const r = scan([join(dir, 'bundle.tar.gz')], null, false);
|
|
expect(r.errors).toEqual([]);
|
|
const shown = r.hits.map(h => `${h.file}: ${h.what}`);
|
|
expect(shown.some(s => s.startsWith('bundle.tar.gz!/') && s.includes('leak.bin') && s.includes('home folder'))).toBe(true);
|
|
expect(shown.some(s => s.includes('My.app.dSYM: debug file'))).toBe(true);
|
|
expect(r.files).toBeGreaterThanOrEqual(3);
|
|
const clean = scan([join(dir, 'pkg/My.app/Contents/clean.txt')], null, false);
|
|
expect(clean.hits).toEqual([]);
|
|
expect(scan([join(dir, 'missing.dmg')], null, false).errors).toHaveLength(1);
|
|
});
|
|
|
|
test('container kinds', () => {
|
|
expect(kindOf('a/storebound_0.1.0_universal.dmg', new Uint8Array())).toBe('dmg');
|
|
expect(kindOf('storebound_0.1.0_universal.app.tar.gz', new Uint8Array())).toBe('tar');
|
|
expect(kindOf('storebound_0.1.0_amd64.deb', new Uint8Array())).toBe('deb');
|
|
expect(kindOf('storebound_0.1.0_amd64.AppImage', new Uint8Array())).toBe('appimage');
|
|
expect(kindOf('storebound_0.1.0_x64-setup.exe', Buffer.from('MZ....Nullsoft Install System'))).toBe('nsis');
|
|
expect(kindOf('storebound-desktop.exe', Buffer.from('MZ'))).toBe('file');
|
|
});
|
|
test('AppImage: the squashfs starts where the ELF runtime ends, not at an "hsqs" constant inside the runtime', () => {
|
|
const img = Buffer.alloc(0x1000 + 16);
|
|
img.write('\x7fELF', 0, 'latin1'); img[4] = 2; img[5] = 1;
|
|
img.writeBigUInt64LE(0xc00n, 0x28); img.writeUInt16LE(64, 0x3a); img.writeUInt16LE(16, 0x3c); // 0xc00 + 64*16 = 0x1000
|
|
img.write('hsqs', 0x800, 'latin1'); img.write('hsqs', 0x1000, 'latin1');
|
|
expect(squashfsOffset(img)).toBe(0x1000);
|
|
const plain = Buffer.alloc(0x900); plain.write('hsqs', 0x800, 'latin1');
|
|
expect(squashfsOffset(plain)).toBe(0x800);
|
|
});
|
|
});
|