@intincrab/updiff 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -18
- package/dist/agent.js +1 -1
- package/dist/bundle.js +63 -5
- package/dist/cask-history.js +216 -0
- package/dist/cask.js +79 -0
- package/dist/codesign.js +41 -0
- package/dist/commands.js +156 -5
- package/dist/diff.js +99 -3
- package/dist/extensions.js +453 -0
- package/dist/fetch.js +81 -0
- package/dist/hosts.js +17 -0
- package/dist/installer.js +177 -0
- package/dist/macho.js +35 -0
- package/dist/pkg-report.js +211 -0
- package/dist/pkg.js +211 -0
- package/dist/report.js +31 -2
- package/dist/store.js +21 -0
- package/dist/util.js +34 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -6,7 +6,11 @@ import { parseArgs } from 'node:util';
|
|
|
6
6
|
import { AGENT_LABEL, agentPlist, agentStatus, installAgent, notify, uninstallAgent } from "./agent.js";
|
|
7
7
|
import { quickFingerprint, readIdentity, scanBundle } from "./bundle.js";
|
|
8
8
|
import { diffSnapshots, filterBySeverity } from "./diff.js";
|
|
9
|
+
import { diffExtensions, discoverExtensions, renderExtensions } from "./extensions.js";
|
|
10
|
+
import { caskHistory, fetchRelease, pickRelease, resolveCask, sameVersion } from "./fetch.js";
|
|
9
11
|
import { renderJson, renderMarkdown, renderScan, renderText } from "./report.js";
|
|
12
|
+
import { inspectPkg } from "./pkg.js";
|
|
13
|
+
import { diffPkgs, renderPkg, renderPkgMarkdown } from "./pkg-report.js";
|
|
10
14
|
import { Store } from "./store.js";
|
|
11
15
|
import { mapLimit } from "./util.js";
|
|
12
16
|
import { TOOL_VERSION } from "./version.js";
|
|
@@ -19,6 +23,16 @@ Usage:
|
|
|
19
23
|
updiff diff <a> <b> Compare two apps, snapshot files or stored apps
|
|
20
24
|
updiff check Snapshot apps that changed and report the differences
|
|
21
25
|
updiff list [app] List stored snapshots
|
|
26
|
+
updiff fetch <app> [version] Download an earlier release from Homebrew's cask
|
|
27
|
+
history, store it as a snapshot and compare it
|
|
28
|
+
with the installed app (no baseline needed)
|
|
29
|
+
updiff extensions [check] List browser and editor extensions with what they
|
|
30
|
+
can reach; 'check' reports what changed since the
|
|
31
|
+
last check (permissions, sites, bundled code)
|
|
32
|
+
updiff pkg <installer.pkg> [older.pkg]
|
|
33
|
+
Show what an installer package would do: its
|
|
34
|
+
scripts, launch daemons and install locations.
|
|
35
|
+
With two packages, what changed. Nothing is run.
|
|
22
36
|
updiff agent install|uninstall|status
|
|
23
37
|
Run 'check' automatically after app updates
|
|
24
38
|
|
|
@@ -37,12 +51,16 @@ Options:
|
|
|
37
51
|
/Applications and ~/Applications, or
|
|
38
52
|
$UPDIFF_APPS_DIRS, colon-separated)
|
|
39
53
|
--notify check: show a notification for each changed app
|
|
54
|
+
--extensions check: also check browser and editor extensions
|
|
40
55
|
--settle <seconds> check: wait until apps stop changing for this long
|
|
41
56
|
--keep <n> check/snapshot: snapshots to keep per app (default 10)
|
|
42
57
|
--quiet Print less
|
|
43
58
|
--interval <seconds> agent install: fallback timer (default 21600)
|
|
44
59
|
--dry-run agent install: print the LaunchAgent, change nothing
|
|
45
60
|
--no-load agent install/uninstall: write files, skip launchctl
|
|
61
|
+
--no-gatekeeper Skip the Gatekeeper/notarization check (faster)
|
|
62
|
+
--cask <name> fetch: the Homebrew cask, when it isn't the app's name
|
|
63
|
+
--list fetch: list the releases in the cask's history
|
|
46
64
|
--no-color Plain text output
|
|
47
65
|
-h, --help Show this help
|
|
48
66
|
-v, --version Show the version
|
|
@@ -73,6 +91,10 @@ function parse(argv) {
|
|
|
73
91
|
'dry-run': { type: 'boolean' },
|
|
74
92
|
'no-load': { type: 'boolean' },
|
|
75
93
|
'no-color': { type: 'boolean' },
|
|
94
|
+
'no-gatekeeper': { type: 'boolean' },
|
|
95
|
+
cask: { type: 'string' },
|
|
96
|
+
list: { type: 'boolean' },
|
|
97
|
+
extensions: { type: 'boolean' },
|
|
76
98
|
help: { type: 'boolean', short: 'h' },
|
|
77
99
|
version: { type: 'boolean', short: 'v' },
|
|
78
100
|
},
|
|
@@ -184,7 +206,8 @@ async function cmdScan(ctx, args) {
|
|
|
184
206
|
const app = await findAppByName(args[0], ctx.appsDirs);
|
|
185
207
|
if (!app)
|
|
186
208
|
throw new UsageError(`Can't find app "${args[0]}".`);
|
|
187
|
-
|
|
209
|
+
// A one-off scan of an installed app also shows this Mac's update settings for it.
|
|
210
|
+
const s = await scanBundle(app, { ...ctx.scan, userDefaults: true });
|
|
188
211
|
const text = ctx.format === 'json' ? renderJson(s) : renderScan(s, { color: ctx.io.color && !ctx.out });
|
|
189
212
|
if (ctx.out) {
|
|
190
213
|
await writeFile(ctx.out, text);
|
|
@@ -309,7 +332,48 @@ async function waitUntilSettled(apps, seconds, io, quiet) {
|
|
|
309
332
|
await new Promise((r) => setTimeout(r, Math.min(15, seconds - age) * 1000));
|
|
310
333
|
}
|
|
311
334
|
}
|
|
312
|
-
|
|
335
|
+
/** Compares installed extensions with the last stored set and stores the new one. */
|
|
336
|
+
async function checkExtensions(ctx) {
|
|
337
|
+
const current = { schema: 1, takenAt: new Date().toISOString(), extensions: await discoverExtensions() };
|
|
338
|
+
const previous = await ctx.store.latestExtensions();
|
|
339
|
+
if (!previous) {
|
|
340
|
+
await ctx.store.saveExtensions(current);
|
|
341
|
+
return { count: current.extensions.length, baseline: true };
|
|
342
|
+
}
|
|
343
|
+
const report = diffExtensions(previous, current);
|
|
344
|
+
if (report.identical)
|
|
345
|
+
return { count: current.extensions.length, baseline: false };
|
|
346
|
+
await ctx.store.saveExtensions(current);
|
|
347
|
+
const file = await ctx.store.saveReport('extensions', previous.takenAt.slice(0, 10), current.takenAt.slice(0, 10), 'md', renderMarkdown(report));
|
|
348
|
+
return { count: current.extensions.length, baseline: false, report, file };
|
|
349
|
+
}
|
|
350
|
+
function describeExtensionCheck(r) {
|
|
351
|
+
if (r.baseline)
|
|
352
|
+
return `Saved a baseline of ${r.count} extension${r.count === 1 ? '' : 's'}; the next check reports what changes.`;
|
|
353
|
+
return r.report ? `Extensions: ${r.report.counts.high} high, ${r.report.counts.notable} notable, ${r.report.counts.info} info.` : `No changes in ${r.count} extension${r.count === 1 ? '' : 's'}.`;
|
|
354
|
+
}
|
|
355
|
+
async function cmdExtensions(ctx, args) {
|
|
356
|
+
const [action, ...rest] = args;
|
|
357
|
+
if (rest.length || (action && action !== 'check' && action !== 'list'))
|
|
358
|
+
throw new UsageError('extensions takes `list` (default) or `check`.');
|
|
359
|
+
if (action !== 'check') {
|
|
360
|
+
const list = await discoverExtensions();
|
|
361
|
+
ctx.io.out(ctx.format === 'json' ? renderJson(list) : renderExtensions(list, { color: ctx.io.color }));
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
const r = await checkExtensions(ctx);
|
|
365
|
+
if (ctx.format === 'json') {
|
|
366
|
+
ctx.io.out(renderJson({ count: r.count, baseline: r.baseline, ...(r.report ? { changes: filterBySeverity(r.report, ctx.minSeverity), reportFile: r.file } : {}) }));
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
if (r.report) {
|
|
370
|
+
ctx.io.out(renderText(filterBySeverity(r.report, ctx.minSeverity), { color: ctx.io.color, full: ctx.full }));
|
|
371
|
+
ctx.io.out(`Report: ${r.file}\n\n`);
|
|
372
|
+
}
|
|
373
|
+
ctx.io.out(`${describeExtensionCheck(r)}\n`);
|
|
374
|
+
return 0;
|
|
375
|
+
}
|
|
376
|
+
async function cmdCheck(ctx, args, notifyEnabled, settle, withExtensions) {
|
|
313
377
|
if (args.length)
|
|
314
378
|
throw new UsageError('check takes no app names; use --apps-dir to choose folders.');
|
|
315
379
|
let apps = await discoverApps(ctx.appsDirs);
|
|
@@ -326,12 +390,14 @@ async function cmdCheck(ctx, args, notifyEnabled, settle) {
|
|
|
326
390
|
}
|
|
327
391
|
const baseline = results.filter((r) => r.status === 'new');
|
|
328
392
|
const failed = results.filter((r) => r.status === 'failed');
|
|
393
|
+
const ext = withExtensions ? await checkExtensions(ctx) : undefined;
|
|
329
394
|
if (ctx.format === 'json') {
|
|
330
395
|
ctx.io.out(renderJson({
|
|
331
396
|
checked: apps.length,
|
|
332
397
|
baseline: baseline.map((r) => r.snapshot.app.bundleId),
|
|
333
398
|
failed: failed.map((r) => ({ app: r.app, error: r.error })),
|
|
334
399
|
changed: reports.map(({ report, file }) => ({ ...filterBySeverity(report, ctx.minSeverity), reportFile: file })),
|
|
400
|
+
...(ext ? { extensions: { count: ext.count, baseline: ext.baseline, ...(ext.report ? { changes: filterBySeverity(ext.report, ctx.minSeverity), reportFile: ext.file } : {}) } } : {}),
|
|
335
401
|
}));
|
|
336
402
|
}
|
|
337
403
|
else {
|
|
@@ -347,6 +413,12 @@ async function cmdCheck(ctx, args, notifyEnabled, settle) {
|
|
|
347
413
|
parts.push(`${failed.length} failed (${failed.map((f) => basename(f.app)).join(', ')})`);
|
|
348
414
|
ctx.io.out(`${parts.join(', ')}.\n`);
|
|
349
415
|
}
|
|
416
|
+
if (ext?.report) {
|
|
417
|
+
ctx.io.out('\n' + renderText(filterBySeverity(ext.report, ctx.minSeverity), { color: ctx.io.color, full: ctx.full }));
|
|
418
|
+
ctx.io.out(`Report: ${ext.file}\n`);
|
|
419
|
+
}
|
|
420
|
+
if (ext && (!ctx.quiet || ext.report))
|
|
421
|
+
ctx.io.out(`${describeExtensionCheck(ext)}\n`);
|
|
350
422
|
}
|
|
351
423
|
if (notifyEnabled) {
|
|
352
424
|
for (const { report } of reports) {
|
|
@@ -354,7 +426,80 @@ async function cmdCheck(ctx, args, notifyEnabled, settle) {
|
|
|
354
426
|
const summary = high || notable ? `${high} high, ${notable} notable change${high + notable === 1 ? '' : 's'}` : 'No notable changes';
|
|
355
427
|
await notify(`Updiff: ${report.app.name} ${report.to.version}`, `${summary}. Run: updiff diff "${report.app.bundleId}"`);
|
|
356
428
|
}
|
|
429
|
+
if (ext?.report && (ext.report.counts.high || ext.report.counts.notable)) {
|
|
430
|
+
await notify('Updiff: extensions changed', `${ext.report.counts.high} high, ${ext.report.counts.notable} notable. Report: ${ext.file}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return 0;
|
|
434
|
+
}
|
|
435
|
+
async function cmdFetch(ctx, args, opts) {
|
|
436
|
+
if (args.length < 1 || args.length > 2)
|
|
437
|
+
throw new UsageError('fetch needs an app (or cask) and optionally a version.');
|
|
438
|
+
const [target, wanted] = args;
|
|
439
|
+
const installed = await findAppByName(target, ctx.appsDirs);
|
|
440
|
+
const identity = installed ? await readIdentity(installed) : undefined;
|
|
441
|
+
const candidates = opts.cask ? [opts.cask] : [target, ...(identity ? [identity.name, basename(installed, '.app')] : [])];
|
|
442
|
+
const log = (s) => {
|
|
443
|
+
if (!ctx.quiet)
|
|
444
|
+
ctx.io.err(`${s}\n`);
|
|
445
|
+
};
|
|
446
|
+
const info = await resolveCask(candidates);
|
|
447
|
+
log(`Reading the history of the Homebrew cask "${info.token}"…`);
|
|
448
|
+
const { releases } = await caskHistory(info.token, { info });
|
|
449
|
+
if (!releases.length)
|
|
450
|
+
throw new UsageError(`Found no releases in the history of the ${info.token} cask.`);
|
|
451
|
+
if (opts.list) {
|
|
452
|
+
const rows = releases.map((r) => ({
|
|
453
|
+
version: r.version,
|
|
454
|
+
date: r.date.slice(0, 10),
|
|
455
|
+
checksum: r.sha256 ? 'pinned' : 'not pinned',
|
|
456
|
+
url: r.url ?? '(derived from the current URL)',
|
|
457
|
+
installed: sameVersion(r.version, identity?.version),
|
|
458
|
+
}));
|
|
459
|
+
if (ctx.format === 'json')
|
|
460
|
+
ctx.io.out(renderJson(rows));
|
|
461
|
+
else {
|
|
462
|
+
const w = Math.max(...rows.map((r) => r.version.length), 7);
|
|
463
|
+
for (const r of rows)
|
|
464
|
+
ctx.io.out(`${r.version.padEnd(w)} ${r.date} ${r.checksum.padEnd(10)} ${r.installed ? 'installed' : ''}\n`);
|
|
465
|
+
}
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
const release = pickRelease(releases, wanted, identity?.version);
|
|
469
|
+
const fetched = await fetchRelease(info, release, { scan: ctx.scan, log });
|
|
470
|
+
const file = await ctx.store.save(fetched.snapshot);
|
|
471
|
+
log(`Stored ${fetched.snapshot.app.name} ${fetched.snapshot.app.version} as a snapshot (${file}).`);
|
|
472
|
+
if (!installed) {
|
|
473
|
+
log(`${info.name} isn't installed here, so there's nothing to compare it with yet.`);
|
|
474
|
+
return 0;
|
|
475
|
+
}
|
|
476
|
+
if (identity && identity.bundleId !== fetched.snapshot.app.bundleId) {
|
|
477
|
+
ctx.io.err(`Warning: the cask's app (${fetched.snapshot.app.bundleId}) isn't the installed app (${identity.bundleId}); check --cask.\n`);
|
|
478
|
+
}
|
|
479
|
+
log(`Comparing with the installed ${identity?.name ?? basename(installed)} ${identity?.version ?? ''}…\n`);
|
|
480
|
+
await emit(ctx, diffSnapshots(fetched.snapshot, await scanBundle(installed, ctx.scan)));
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
async function cmdPkg(ctx, args) {
|
|
484
|
+
if (args.length < 1 || args.length > 2)
|
|
485
|
+
throw new UsageError('pkg needs an installer package, or two to compare.');
|
|
486
|
+
for (const a of args)
|
|
487
|
+
if (!existsSync(a))
|
|
488
|
+
throw new UsageError(`Can't find "${a}".`);
|
|
489
|
+
const reports = await Promise.all(args.map((a) => inspectPkg(resolve(a))));
|
|
490
|
+
if (reports.length === 2) {
|
|
491
|
+
// Arguments are new then old, matching "what does this update change".
|
|
492
|
+
await emit(ctx, diffPkgs(reports[1], reports[0]));
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
const r = reports[0];
|
|
496
|
+
const text = ctx.format === 'json' ? renderJson(r) : ctx.format === 'md' ? renderPkgMarkdown(r) : renderPkg(r, { color: ctx.io.color && !ctx.out, full: ctx.full });
|
|
497
|
+
if (ctx.out) {
|
|
498
|
+
await writeFile(ctx.out, text);
|
|
499
|
+
ctx.io.err(`Wrote ${ctx.out}\n`);
|
|
357
500
|
}
|
|
501
|
+
else
|
|
502
|
+
ctx.io.out(text);
|
|
358
503
|
return 0;
|
|
359
504
|
}
|
|
360
505
|
async function cmdList(ctx, args) {
|
|
@@ -473,7 +618,7 @@ export async function main(argv, io) {
|
|
|
473
618
|
io: { ...io, color: io.color && !v['no-color'] && format === 'text' },
|
|
474
619
|
store: new Store(v.home ? resolve(v.home) : undefined),
|
|
475
620
|
appsDirs: (v['apps-dir'] ?? defaultAppsDirs()).map((d) => resolve(d)),
|
|
476
|
-
scan: { deep: v.deep === true },
|
|
621
|
+
scan: { deep: v.deep === true, notarization: !v['no-gatekeeper'] },
|
|
477
622
|
format,
|
|
478
623
|
minSeverity,
|
|
479
624
|
out: v.out,
|
|
@@ -490,9 +635,15 @@ export async function main(argv, io) {
|
|
|
490
635
|
case 'diff':
|
|
491
636
|
return await cmdDiff(ctx, args);
|
|
492
637
|
case 'check':
|
|
493
|
-
return await cmdCheck(ctx, args, v.notify === true, intOption(v.settle, 'settle', 0));
|
|
638
|
+
return await cmdCheck(ctx, args, v.notify === true, intOption(v.settle, 'settle', 0), v.extensions === true);
|
|
639
|
+
case 'extensions':
|
|
640
|
+
return await cmdExtensions(ctx, args);
|
|
494
641
|
case 'list':
|
|
495
642
|
return await cmdList(ctx, args);
|
|
643
|
+
case 'pkg':
|
|
644
|
+
return await cmdPkg(ctx, args);
|
|
645
|
+
case 'fetch':
|
|
646
|
+
return await cmdFetch(ctx, args, { cask: v.cask, list: v.list === true });
|
|
496
647
|
case 'agent':
|
|
497
648
|
return await cmdAgent(ctx, args, { interval: Math.max(600, intOption(v.interval, 'interval', 21600)), dryRun: v['dry-run'] === true, load: !v['no-load'] });
|
|
498
649
|
default:
|
|
@@ -504,7 +655,7 @@ export async function main(argv, io) {
|
|
|
504
655
|
io.err(`${err.message}\n`);
|
|
505
656
|
return 2;
|
|
506
657
|
}
|
|
507
|
-
io.err(`updiff: ${err.stack ?? err}\n`);
|
|
658
|
+
io.err(`updiff: ${process.env.UPDIFF_DEBUG ? (err.stack ?? err) : (err.message ?? err)}\n`);
|
|
508
659
|
return 1;
|
|
509
660
|
}
|
|
510
661
|
}
|
package/dist/diff.js
CHANGED
|
@@ -398,6 +398,84 @@ function diffInfo(a, b, out) {
|
|
|
398
398
|
out.push({ severity: 'info', category: 'identity', change: 'changed', title: `Minimum macOS ${ia.minimumSystemVersion ?? '?'} → ${ib.minimumSystemVersion}`, where: ['Contents/Info.plist'] });
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
|
+
/** Apple has vouched for it: notarized, or distributed by Apple (App Store, system). */
|
|
402
|
+
const isApproved = (source) => /notarized|mac app store|apple system/i.test(source ?? '');
|
|
403
|
+
function diffNotarization(a, b, out) {
|
|
404
|
+
const na = a.notarization;
|
|
405
|
+
const nb = b.notarization;
|
|
406
|
+
// Older snapshots (and scans with notarization off) have nothing to compare.
|
|
407
|
+
if (!na || !nb)
|
|
408
|
+
return;
|
|
409
|
+
const where = [b.app.path];
|
|
410
|
+
if (na.assessment === 'accepted' && nb.assessment === 'unknown' && nb.error) {
|
|
411
|
+
out.push({
|
|
412
|
+
severity: 'high',
|
|
413
|
+
category: 'signing',
|
|
414
|
+
change: 'changed',
|
|
415
|
+
title: "Gatekeeper can no longer verify the app's signature",
|
|
416
|
+
detail: nb.error,
|
|
417
|
+
where,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
else if (na.assessment === 'accepted' && nb.assessment === 'rejected') {
|
|
421
|
+
out.push({
|
|
422
|
+
severity: 'high',
|
|
423
|
+
category: 'signing',
|
|
424
|
+
change: 'changed',
|
|
425
|
+
title: 'Gatekeeper now rejects the app',
|
|
426
|
+
detail: `${na.source ?? 'accepted'} → rejected`,
|
|
427
|
+
where,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else if (isApproved(na.source) && !isApproved(nb.source) && nb.assessment !== 'unknown') {
|
|
431
|
+
out.push({
|
|
432
|
+
severity: 'high',
|
|
433
|
+
category: 'signing',
|
|
434
|
+
change: 'removed',
|
|
435
|
+
title: 'No longer notarized by Apple',
|
|
436
|
+
detail: `${na.source} → ${nb.source ?? nb.assessment}`,
|
|
437
|
+
where,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else if (na.source && nb.source && na.source !== nb.source) {
|
|
441
|
+
out.push({ severity: 'notable', category: 'signing', change: 'changed', title: 'Distribution changed', detail: `${na.source} → ${nb.source}`, where });
|
|
442
|
+
}
|
|
443
|
+
if (na.stapled === true && nb.stapled === false) {
|
|
444
|
+
out.push({
|
|
445
|
+
severity: 'info',
|
|
446
|
+
category: 'signing',
|
|
447
|
+
change: 'removed',
|
|
448
|
+
title: 'Notarization ticket no longer stapled',
|
|
449
|
+
detail: 'Gatekeeper checks it online instead; first launch offline may fail.',
|
|
450
|
+
where,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else if (na.stapled === false && nb.stapled === true) {
|
|
454
|
+
out.push({ severity: 'info', category: 'signing', change: 'added', title: 'Notarization ticket now stapled', where });
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function diffSparkleCode(a, b, out) {
|
|
458
|
+
const fa = a.info.sparkleFeedsInCode ?? [];
|
|
459
|
+
const fb = b.info.sparkleFeedsInCode ?? [];
|
|
460
|
+
const added = fb.filter((u) => !fa.includes(u));
|
|
461
|
+
const removed = fa.filter((u) => !fb.includes(u));
|
|
462
|
+
if (added.length && fa.length) {
|
|
463
|
+
out.push({
|
|
464
|
+
severity: 'notable',
|
|
465
|
+
category: 'updater',
|
|
466
|
+
change: 'changed',
|
|
467
|
+
title: 'Update feed in the code changed',
|
|
468
|
+
detail: `${removed.length ? `${removed.join(', ')} → ` : ''}${added.join(', ')}`,
|
|
469
|
+
where: [b.app.path],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
else if (added.length) {
|
|
473
|
+
out.push({ severity: 'notable', category: 'updater', change: 'added', title: 'Update feed set in the code', detail: added.join(', '), where: [b.app.path] });
|
|
474
|
+
}
|
|
475
|
+
else if (removed.length) {
|
|
476
|
+
out.push({ severity: 'info', category: 'updater', change: 'removed', title: 'Update feed no longer in the code', detail: removed.join(', '), where: [b.app.path] });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
401
479
|
function diffElectron(a, b, out) {
|
|
402
480
|
const pa = a.electron?.packages ?? {};
|
|
403
481
|
const pb = b.electron?.packages ?? {};
|
|
@@ -478,9 +556,9 @@ function diffHosts(a, b, out) {
|
|
|
478
556
|
out.push({ severity: 'info', category: 'host', change: 'removed', title: `${gone.length} domain${gone.length === 1 ? '' : 's'} no longer mentioned`, detail: gone.join(', ') });
|
|
479
557
|
}
|
|
480
558
|
}
|
|
481
|
-
const SEVERITY_ORDER = { high: 0, notable: 1, info: 2 };
|
|
482
|
-
const CATEGORY_ORDER = [
|
|
483
|
-
'signing', 'privacy', 'entitlement', 'background', 'updater', 'network', 'code', 'urlScheme', 'electron', 'package', 'host', 'identity',
|
|
559
|
+
export const SEVERITY_ORDER = { high: 0, notable: 1, info: 2 };
|
|
560
|
+
export const CATEGORY_ORDER = [
|
|
561
|
+
'signing', 'privacy', 'permission', 'entitlement', 'background', 'script', 'install', 'updater', 'network', 'code', 'urlScheme', 'electron', 'package', 'host', 'identity',
|
|
484
562
|
];
|
|
485
563
|
export function diffSnapshots(a, b) {
|
|
486
564
|
const findings = [];
|
|
@@ -491,6 +569,16 @@ export function diffSnapshots(a, b) {
|
|
|
491
569
|
diffCode(a, b, findings);
|
|
492
570
|
diffBackground(a.backgroundItems, b.backgroundItems, findings);
|
|
493
571
|
diffElectron(a, b, findings);
|
|
572
|
+
diffNotarization(a, b, findings);
|
|
573
|
+
diffSparkleCode(a, b, findings);
|
|
574
|
+
if (!a.info.appStoreReceipt !== !b.info.appStoreReceipt) {
|
|
575
|
+
findings.push({
|
|
576
|
+
severity: 'notable',
|
|
577
|
+
category: 'identity',
|
|
578
|
+
change: b.info.appStoreReceipt ? 'added' : 'removed',
|
|
579
|
+
title: b.info.appStoreReceipt ? 'Now distributed through the Mac App Store' : 'No longer a Mac App Store build',
|
|
580
|
+
});
|
|
581
|
+
}
|
|
494
582
|
diffHosts(a, b, findings);
|
|
495
583
|
const substantive = findings.length;
|
|
496
584
|
if (a.app.version !== b.app.version || a.app.build !== b.app.build) {
|
|
@@ -507,6 +595,13 @@ export function diffSnapshots(a, b) {
|
|
|
507
595
|
const counts = { high: 0, notable: 0, info: 0 };
|
|
508
596
|
for (const f of findings)
|
|
509
597
|
counts[f.severity]++;
|
|
598
|
+
const notes = [];
|
|
599
|
+
const encrypted = [...new Set([...(a.encryptedBinaries ?? []), ...(b.encryptedBinaries ?? [])])];
|
|
600
|
+
if (encrypted.length) {
|
|
601
|
+
notes.push(`Encrypted binaries (an iPhone or iPad app) can't be searched for hostnames: ${encrypted.slice(0, 3).join(', ')}${encrypted.length > 3 ? '…' : ''}.`);
|
|
602
|
+
}
|
|
603
|
+
if (!a.notarization !== !b.notarization)
|
|
604
|
+
notes.push('Notarization was only checked on one side, so it was not compared.');
|
|
510
605
|
return {
|
|
511
606
|
app: { name: b.app.name, bundleId: b.app.bundleId },
|
|
512
607
|
from: { version: a.app.version, build: a.app.build, takenAt: a.takenAt, path: a.app.path },
|
|
@@ -514,6 +609,7 @@ export function diffSnapshots(a, b) {
|
|
|
514
609
|
findings,
|
|
515
610
|
counts,
|
|
516
611
|
identical: substantive === 0 && a.fingerprint === b.fingerprint,
|
|
612
|
+
...(notes.length ? { notes } : {}),
|
|
517
613
|
};
|
|
518
614
|
}
|
|
519
615
|
export function filterBySeverity(report, min) {
|