Skip to content

Commit f451f9c

Browse files
committed
fix(release): keep privacy scan inside artifacts
1 parent 305dd53 commit f451f9c

4 files changed

Lines changed: 65 additions & 13 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { spawnSync } from 'node:child_process';
2+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
3+
import { tmpdir } from 'node:os';
4+
import { join, resolve } from 'node:path';
5+
import { afterEach, describe, expect, it } from 'vitest';
6+
7+
const tempRoots: string[] = [];
8+
9+
afterEach(() => {
10+
for (const root of tempRoots.splice(0)) rmSync(root, { force: true, recursive: true });
11+
});
12+
13+
describe('release privacy verification', () => {
14+
it('checks symlinks without following them outside the artifact', () => {
15+
const tempRoot = mkdtempSync(join(tmpdir(), 'voyager-release-privacy-'));
16+
tempRoots.push(tempRoot);
17+
18+
const artifact = join(tempRoot, 'artifact');
19+
const externalDirectory = join(tempRoot, 'external');
20+
mkdirSync(artifact);
21+
mkdirSync(externalDirectory);
22+
writeFileSync(join(artifact, 'safe.txt'), 'safe release content');
23+
writeFileSync(join(externalDirectory, 'private.txt'), '/Users/private-owner/secret');
24+
symlinkSync(externalDirectory, join(artifact, 'Applications'));
25+
26+
const result = spawnSync(
27+
process.execPath,
28+
[resolve(process.cwd(), 'scripts/verify-release-privacy.mjs'), artifact],
29+
{ encoding: 'utf8' },
30+
);
31+
32+
expect(result.status, result.stderr).toBe(0);
33+
expect(result.stdout).toContain('Release privacy check passed (1 files, 1 symlinks)');
34+
});
35+
});

scripts/build-safari-release.sh

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,8 @@ submit_for_notarization() {
5959
fi
6060

6161
local notary_status
62-
local submission_id
6362
notary_status=$(plutil -extract status raw -o - "$result_file")
64-
submission_id=$(plutil -extract id raw -o - "$result_file")
65-
printf 'Notarization %s (submission %s)\n' "$notary_status" "$submission_id"
63+
printf 'Notarization %s\n' "$notary_status"
6664

6765
if [[ $notary_status != Accepted ]]; then
6866
echo "Apple notarization rejected: $artifact" >&2

scripts/verify-release-privacy.mjs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22

3-
import { readFile, readdir, stat } from 'node:fs/promises';
3+
import { lstat, readFile, readdir, readlink } from 'node:fs/promises';
44
import { basename, join } from 'node:path';
55

66
const roots = process.argv.slice(2);
@@ -37,9 +37,15 @@ const forbiddenPatterns = [
3737
];
3838

3939
const files = [];
40+
const symlinks = [];
4041

4142
async function collect(path) {
42-
const metadata = await stat(path);
43+
const metadata = await lstat(path);
44+
if (metadata.isSymbolicLink()) {
45+
symlinks.push({ path, target: await readlink(path) });
46+
return;
47+
}
48+
4349
if (!metadata.isDirectory()) {
4450
files.push(path);
4551
return;
@@ -53,25 +59,37 @@ async function collect(path) {
5359
for (const root of roots) await collect(root);
5460

5561
const failures = [];
62+
function scanContent(path, content) {
63+
for (const [needle, label] of forbiddenContent) {
64+
if (content.includes(Buffer.from(needle))) failures.push(`${path}: contains ${label}`);
65+
}
66+
const text = content.toString('latin1');
67+
for (const [pattern, label] of forbiddenPatterns) {
68+
if (pattern.test(text)) failures.push(`${path}: contains ${label}`);
69+
}
70+
}
71+
5672
for (const file of files) {
5773
if (forbiddenNames.some((pattern) => pattern.test(basename(file)))) {
5874
failures.push(`${file}: forbidden release filename`);
5975
continue;
6076
}
6177

62-
const content = await readFile(file);
63-
for (const [needle, label] of forbiddenContent) {
64-
if (content.includes(Buffer.from(needle))) failures.push(`${file}: contains ${label}`);
65-
}
66-
const text = content.toString('latin1');
67-
for (const [pattern, label] of forbiddenPatterns) {
68-
if (pattern.test(text)) failures.push(`${file}: contains ${label}`);
78+
scanContent(file, await readFile(file));
79+
}
80+
81+
for (const { path, target } of symlinks) {
82+
if (forbiddenNames.some((pattern) => pattern.test(basename(path)))) {
83+
failures.push(`${path}: forbidden release filename`);
84+
continue;
6985
}
86+
87+
scanContent(`${path} -> ${target}`, Buffer.from(target));
7088
}
7189

7290
if (failures.length > 0) {
7391
console.error(`Release privacy check failed:\n${failures.join('\n')}`);
7492
process.exit(1);
7593
}
7694

77-
console.log(`Release privacy check passed (${files.length} files)`);
95+
console.log(`Release privacy check passed (${files.length} files, ${symlinks.length} symlinks)`);

src/pages/popup/__tests__/releaseArtifacts.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('release artifacts', () => {
3636

3737
expect(script).toContain('local notary_status');
3838
expect(script).not.toMatch(/\blocal status\b/);
39+
expect(script).not.toContain('submission_id');
3940
});
4041

4142
it('builds and stores the Edge release variant independently from Chrome', () => {

0 commit comments

Comments
 (0)