monkeypatchlist 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +61 -0
- package/README.md +183 -2
- package/catalog/catalog.json +552211 -0
- package/catalog/catalog.schema.json +787 -0
- package/catalog/changelog.json +3 -0
- package/dist/bin/monkeypatchlist.d.ts +2 -0
- package/dist/bin/monkeypatchlist.js +3 -0
- package/dist/catalog.d.ts +561 -0
- package/dist/catalog.js +58 -0
- package/dist/cli.d.ts +84 -0
- package/dist/cli.js +470 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/lockfile-input.d.ts +9 -0
- package/dist/lockfile-input.js +91 -0
- package/dist/packages/analysis/binding.d.ts +2 -0
- package/dist/packages/analysis/binding.js +5 -0
- package/dist/packages/analysis/index.d.ts +10 -0
- package/dist/packages/analysis/index.js +13 -0
- package/dist/packages/analysis/job.d.ts +33 -0
- package/dist/packages/analysis/job.js +85 -0
- package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
- package/dist/packages/analysis/lib/absence-tags.js +340 -0
- package/dist/packages/analysis/lib/ast.d.ts +28 -0
- package/dist/packages/analysis/lib/ast.js +64 -0
- package/dist/packages/analysis/lib/binding.d.ts +36 -0
- package/dist/packages/analysis/lib/binding.js +66 -0
- package/dist/packages/analysis/lib/budget.d.ts +80 -0
- package/dist/packages/analysis/lib/budget.js +31 -0
- package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
- package/dist/packages/analysis/lib/catch-arms.js +351 -0
- package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
- package/dist/packages/analysis/lib/chunk-loads.js +882 -0
- package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
- package/dist/packages/analysis/lib/chunk-registry.js +334 -0
- package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
- package/dist/packages/analysis/lib/code-routes.js +921 -0
- package/dist/packages/analysis/lib/collisions.d.ts +22 -0
- package/dist/packages/analysis/lib/collisions.js +103 -0
- package/dist/packages/analysis/lib/definitions.d.ts +76 -0
- package/dist/packages/analysis/lib/definitions.js +782 -0
- package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
- package/dist/packages/analysis/lib/early-exits.js +161 -0
- package/dist/packages/analysis/lib/effect.d.ts +138 -0
- package/dist/packages/analysis/lib/effect.js +800 -0
- package/dist/packages/analysis/lib/global-object.d.ts +581 -0
- package/dist/packages/analysis/lib/global-object.js +8240 -0
- package/dist/packages/analysis/lib/keys.d.ts +33 -0
- package/dist/packages/analysis/lib/keys.js +82 -0
- package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
- package/dist/packages/analysis/lib/line-starts.js +79 -0
- package/dist/packages/analysis/lib/link-group.d.ts +51 -0
- package/dist/packages/analysis/lib/link-group.js +487 -0
- package/dist/packages/analysis/lib/module-links.d.ts +117 -0
- package/dist/packages/analysis/lib/module-links.js +1060 -0
- package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
- package/dist/packages/analysis/lib/nested-code.js +200 -0
- package/dist/packages/analysis/lib/numeric.d.ts +16 -0
- package/dist/packages/analysis/lib/numeric.js +216 -0
- package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
- package/dist/packages/analysis/lib/once-flags.js +190 -0
- package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
- package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
- package/dist/packages/analysis/lib/parse.d.ts +10 -0
- package/dist/packages/analysis/lib/parse.js +39 -0
- package/dist/packages/analysis/lib/partial.d.ts +60 -0
- package/dist/packages/analysis/lib/partial.js +518 -0
- package/dist/packages/analysis/lib/print.d.ts +3 -0
- package/dist/packages/analysis/lib/print.js +150 -0
- package/dist/packages/analysis/lib/reachability.d.ts +137 -0
- package/dist/packages/analysis/lib/reachability.js +1295 -0
- package/dist/packages/analysis/lib/resolver.d.ts +13 -0
- package/dist/packages/analysis/lib/resolver.js +2886 -0
- package/dist/packages/analysis/lib/restores.d.ts +42 -0
- package/dist/packages/analysis/lib/restores.js +482 -0
- package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
- package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
- package/dist/packages/analysis/lib/scope.d.ts +110 -0
- package/dist/packages/analysis/lib/scope.js +443 -0
- package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
- package/dist/packages/analysis/lib/script-loads.js +359 -0
- package/dist/packages/analysis/lib/summaries.d.ts +177 -0
- package/dist/packages/analysis/lib/summaries.js +498 -0
- package/dist/packages/analysis/lib/types.d.ts +632 -0
- package/dist/packages/analysis/lib/types.js +128 -0
- package/dist/packages/analysis/lib/wrap.d.ts +34 -0
- package/dist/packages/analysis/lib/wrap.js +407 -0
- package/dist/packages/analysis/lib/writes.d.ts +358 -0
- package/dist/packages/analysis/lib/writes.js +1612 -0
- package/dist/packages/analysis/visit-order.d.ts +3 -0
- package/dist/packages/analysis/visit-order.js +23 -0
- package/dist/packages/attribution/index.d.ts +233 -0
- package/dist/packages/attribution/index.js +444 -0
- package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
- package/dist/packages/attribution/lib/containing-package.js +304 -0
- package/dist/packages/attribution/lib/first-party.d.ts +37 -0
- package/dist/packages/attribution/lib/first-party.js +153 -0
- package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
- package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
- package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
- package/dist/packages/attribution/lib/lockfile.js +330 -0
- package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
- package/dist/packages/attribution/lib/path-rules.js +84 -0
- package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
- package/dist/packages/attribution/lib/pnpm.js +133 -0
- package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
- package/dist/packages/attribution/lib/remote-url.js +24 -0
- package/dist/packages/attribution/lib/source-map.d.ts +51 -0
- package/dist/packages/attribution/lib/source-map.js +125 -0
- package/dist/packages/attribution/lib/yaml.d.ts +9 -0
- package/dist/packages/attribution/lib/yaml.js +277 -0
- package/dist/packages/attribution/lib/yarn.d.ts +7 -0
- package/dist/packages/attribution/lib/yarn.js +227 -0
- package/dist/packages/baseline/index.d.ts +45 -0
- package/dist/packages/baseline/index.js +41 -0
- package/dist/packages/baseline/lib/error.d.ts +15 -0
- package/dist/packages/baseline/lib/error.js +25 -0
- package/dist/packages/baseline/lib/format.d.ts +49 -0
- package/dist/packages/baseline/lib/format.js +22 -0
- package/dist/packages/baseline/lib/migrations.d.ts +19 -0
- package/dist/packages/baseline/lib/migrations.js +7 -0
- package/dist/packages/baseline/lib/previous.d.ts +23 -0
- package/dist/packages/baseline/lib/previous.js +64 -0
- package/dist/packages/baseline/lib/read.d.ts +39 -0
- package/dist/packages/baseline/lib/read.js +219 -0
- package/dist/packages/baseline/lib/report.d.ts +25 -0
- package/dist/packages/baseline/lib/report.js +104 -0
- package/dist/packages/baseline/lib/skew.d.ts +51 -0
- package/dist/packages/baseline/lib/skew.js +75 -0
- package/dist/packages/compare/index.d.ts +120 -0
- package/dist/packages/compare/index.js +274 -0
- package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
- package/dist/packages/compare/lib/catalog-notes.js +224 -0
- package/dist/packages/completeness/index.d.ts +246 -0
- package/dist/packages/completeness/index.js +249 -0
- package/dist/packages/confidence/index.d.ts +54 -0
- package/dist/packages/confidence/index.js +71 -0
- package/dist/packages/config/index.d.ts +107 -0
- package/dist/packages/config/index.js +95 -0
- package/dist/packages/config/lib/command-line.d.ts +23 -0
- package/dist/packages/config/lib/command-line.js +239 -0
- package/dist/packages/config/lib/config-file.d.ts +11 -0
- package/dist/packages/config/lib/config-file.js +119 -0
- package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
- package/dist/packages/config/lib/scan-config-id.js +54 -0
- package/dist/packages/config/lib/schema.d.ts +2 -0
- package/dist/packages/config/lib/schema.js +31 -0
- package/dist/packages/config/lib/types.d.ts +64 -0
- package/dist/packages/config/lib/types.js +6 -0
- package/dist/packages/config/lib/urls.d.ts +6 -0
- package/dist/packages/config/lib/urls.js +9 -0
- package/dist/packages/discovery/index.d.ts +277 -0
- package/dist/packages/discovery/index.js +1950 -0
- package/dist/packages/discovery/lib/import-map.d.ts +68 -0
- package/dist/packages/discovery/lib/import-map.js +180 -0
- package/dist/packages/discovery/lib/requests.d.ts +58 -0
- package/dist/packages/discovery/lib/requests.js +591 -0
- package/dist/packages/discovery/lib/scopes.d.ts +6 -0
- package/dist/packages/discovery/lib/scopes.js +286 -0
- package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
- package/dist/packages/discovery/lib/source-maps.js +209 -0
- package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
- package/dist/packages/discovery/lib/swept-code.js +107 -0
- package/dist/packages/discovery/lib/syntax.d.ts +17 -0
- package/dist/packages/discovery/lib/syntax.js +39 -0
- package/dist/packages/findings/index.d.ts +106 -0
- package/dist/packages/findings/index.js +254 -0
- package/dist/packages/findings/lib/scope.d.ts +38 -0
- package/dist/packages/findings/lib/scope.js +110 -0
- package/dist/packages/parallel/index.d.ts +2 -0
- package/dist/packages/parallel/index.js +5 -0
- package/dist/packages/parallel/lib/protocol.d.ts +15 -0
- package/dist/packages/parallel/lib/protocol.js +2 -0
- package/dist/packages/parallel/lib/runner.d.ts +36 -0
- package/dist/packages/parallel/lib/runner.js +96 -0
- package/dist/packages/parallel/lib/schedule.d.ts +16 -0
- package/dist/packages/parallel/lib/schedule.js +52 -0
- package/dist/packages/parallel/lib/worker.d.ts +1 -0
- package/dist/packages/parallel/lib/worker.js +18 -0
- package/dist/packages/policy/index.d.ts +36 -0
- package/dist/packages/policy/index.js +22 -0
- package/dist/packages/report/explain.d.ts +26 -0
- package/dist/packages/report/explain.js +99 -0
- package/dist/packages/report/index.d.ts +210 -0
- package/dist/packages/report/index.js +388 -0
- package/dist/packages/report/terminal.d.ts +11 -0
- package/dist/packages/report/terminal.js +331 -0
- package/dist/packages/source-store/fixture.d.ts +22 -0
- package/dist/packages/source-store/fixture.js +58 -0
- package/dist/packages/source-store/index.d.ts +278 -0
- package/dist/packages/source-store/index.js +322 -0
- package/dist/packages/source-store/lib/addresses.d.ts +6 -0
- package/dist/packages/source-store/lib/addresses.js +91 -0
- package/dist/packages/source-store/lib/connection.d.ts +15 -0
- package/dist/packages/source-store/lib/connection.js +135 -0
- package/dist/packages/source-store/lib/content-types.d.ts +16 -0
- package/dist/packages/source-store/lib/content-types.js +75 -0
- package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
- package/dist/packages/source-store/lib/data-urls.js +85 -0
- package/dist/packages/source-store/lib/deadline.d.ts +18 -0
- package/dist/packages/source-store/lib/deadline.js +44 -0
- package/dist/packages/source-store/lib/exchange.d.ts +29 -0
- package/dist/packages/source-store/lib/exchange.js +112 -0
- package/dist/packages/source-store/lib/integrity.d.ts +19 -0
- package/dist/packages/source-store/lib/integrity.js +86 -0
- package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
- package/dist/packages/source-store/lib/outcomes.js +22 -0
- package/dist/packages/source-store/lib/proxy.d.ts +11 -0
- package/dist/packages/source-store/lib/proxy.js +53 -0
- package/dist/packages/source-store/lib/session.d.ts +12 -0
- package/dist/packages/source-store/lib/session.js +139 -0
- package/dist/packages/source-store/lib/urls.d.ts +6 -0
- package/dist/packages/source-store/lib/urls.js +15 -0
- package/dist/packages/source-store/resolve.d.ts +59 -0
- package/dist/packages/source-store/resolve.js +45 -0
- package/dist/scan.d.ts +65 -0
- package/dist/scan.js +592 -0
- package/package.json +95 -4
- package/schema/config.schema.json +133 -0
- package/schema/report.schema.json +1905 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// The terminal report (§15 Terminal): change first, then completeness, what each missing input costs (§2 Input tiers),
|
|
2
|
+
// what the surface left out, the ineffective writes and the gate. Every line reads a field the JSON report holds;
|
|
3
|
+
// nothing is recomputed. It is pure: whether to use color is the caller's to decide (a TTY, `NO_COLOR`, `--no-color`).
|
|
4
|
+
import { surfaces } from "../../catalog.js";
|
|
5
|
+
import { limitationKinds } from "../analysis/index.js";
|
|
6
|
+
import { strongVersionSources } from "../attribution/index.js";
|
|
7
|
+
import { compareStrings } from "../findings/index.js";
|
|
8
|
+
const defaults = { color: false, verbose: false, listIneffective: false };
|
|
9
|
+
/** The SGR codes the report uses. */
|
|
10
|
+
const styles = { bold: 1, red: 31, green: 32, yellow: 33, dim: 2 };
|
|
11
|
+
/** The terminal report for `report` (§15 Terminal). */
|
|
12
|
+
export function renderTerminal(report, options = {}) {
|
|
13
|
+
const { color, verbose, listIneffective } = { ...defaults, ...options };
|
|
14
|
+
const paint = color ? (style, text) => `\u001b[${styles[style]}m${text}\u001b[0m` : (_, text) => text;
|
|
15
|
+
const lines = [title(report), ''];
|
|
16
|
+
lines.push(...changes(report, paint, verbose), '');
|
|
17
|
+
lines.push(...completeness(report, paint));
|
|
18
|
+
lines.push(...inputs(report));
|
|
19
|
+
lines.push(outsideScope(report));
|
|
20
|
+
lines.push(...ineffective(report, listIneffective));
|
|
21
|
+
lines.push('', gateLine(report, paint));
|
|
22
|
+
return `${lines.join('\n')}\n`;
|
|
23
|
+
}
|
|
24
|
+
/** `monkeypatchlist <version> · catalog <n> · <name> · surface <surface> · <mode>`. */
|
|
25
|
+
function title({ tool, application, config }) {
|
|
26
|
+
return [
|
|
27
|
+
`monkeypatchlist ${tool.monkeypatchlistVersion}`,
|
|
28
|
+
`catalog ${tool.catalogVersion}`,
|
|
29
|
+
application.name,
|
|
30
|
+
`surface ${config.surface}`,
|
|
31
|
+
config.mode,
|
|
32
|
+
].join(' · ');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The change since the baseline: the counts, a card per `added` and `changed` finding (and per `existing` one under
|
|
36
|
+
* `--verbose`), a line per `removed` and `unconfirmed` entry, and the notes. Without a baseline no finding has a state,
|
|
37
|
+
* so every finding gets a card.
|
|
38
|
+
*/
|
|
39
|
+
function changes(report, paint, verbose) {
|
|
40
|
+
const { summary, findings } = report;
|
|
41
|
+
if (report.baseline === null) {
|
|
42
|
+
if (findings.length === 0)
|
|
43
|
+
return ['No platform mutations found.'];
|
|
44
|
+
return [
|
|
45
|
+
`${paint('bold', 'Findings')} ${findings.length} (no baseline: run with --baseline to see the change)`,
|
|
46
|
+
...findings.flatMap((finding) => card(report, finding)),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
const lines = [
|
|
50
|
+
paint('bold', 'Change since baseline'),
|
|
51
|
+
` Added ${summary.added} Changed ${summary.changed} Removed ${summary.removed} ` +
|
|
52
|
+
`Unconfirmed ${summary.unconfirmed} Existing ${summary.existing}`,
|
|
53
|
+
];
|
|
54
|
+
const inState = (state) => findings.filter((finding) => finding.baselineState === state);
|
|
55
|
+
const section = (heading, style, body) => body.length === 0 ? [] : ['', paint(style, heading), ...body];
|
|
56
|
+
lines.push(...section('Added', 'yellow', inState('added').flatMap((finding) => card(report, finding))));
|
|
57
|
+
lines.push(...section('Changed', 'yellow', inState('changed').flatMap((finding) => card(report, finding))));
|
|
58
|
+
lines.push(...section('Removed', 'bold', inState('removed').map(entryLine)));
|
|
59
|
+
lines.push(...section('Unconfirmed', 'bold', inState('unconfirmed').map(entryLine)));
|
|
60
|
+
if (verbose)
|
|
61
|
+
lines.push(...section('Existing', 'bold', inState('existing').flatMap((finding) => card(report, finding))));
|
|
62
|
+
const notes = [
|
|
63
|
+
...report.baseline.notes.map(skewLine),
|
|
64
|
+
...findings.flatMap((finding) => finding.notes.map((note) => noteLine(finding, note))),
|
|
65
|
+
];
|
|
66
|
+
lines.push(...section('Notes', 'bold', notes));
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
/** The width of a card's or an entry's target column. */
|
|
70
|
+
const targetWidth = 32;
|
|
71
|
+
/** A finding's target as a card names it: its name, and its realm when that isn't `window`. */
|
|
72
|
+
function targetName({ target }) {
|
|
73
|
+
return target.realm === 'window' ? target.name : `${target.name} (${target.realm})`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* A finding's card (§15 Terminal): its target and effect, led for a `changed` one by its `changedReasons` before →
|
|
77
|
+
* after, then Package (with its Previous version), Source (original, then generated), Provenance · install context · reachability, Confidence
|
|
78
|
+
* (with its reasons below `high`) and Fingerprint.
|
|
79
|
+
*/
|
|
80
|
+
function card(report, finding) {
|
|
81
|
+
const reasons = finding.changedReasons.map(changedReason).join(' · ');
|
|
82
|
+
const head = ` ${targetName(finding).padEnd(targetWidth)} ${finding.effect}${reasons === '' ? '' : ` ${reasons}`}`;
|
|
83
|
+
const [first] = finding.occurrences;
|
|
84
|
+
const more = finding.occurrences.length - 1;
|
|
85
|
+
const context = [provenanceLabels[finding.attribution.provenance]];
|
|
86
|
+
if (finding.installContext !== null)
|
|
87
|
+
context.push(finding.installContext);
|
|
88
|
+
if (finding.reachability !== null)
|
|
89
|
+
context.push(finding.reachability);
|
|
90
|
+
return [
|
|
91
|
+
head,
|
|
92
|
+
field('Package', packageLine(report, finding)),
|
|
93
|
+
...finding.notes.flatMap((note) => note.kind === 'previous-version' ? [field('Previous version', `${note.package}@${note.before}`)] : []),
|
|
94
|
+
...introducedByLine(report, finding),
|
|
95
|
+
...(first === undefined
|
|
96
|
+
? []
|
|
97
|
+
: [field('Source', `${sourceOf(report, first)}${more > 0 ? ` (+${more} more ${more === 1 ? 'occurrence' : 'occurrences'})` : ''}`)]),
|
|
98
|
+
field('Provenance', context.join(' · ')),
|
|
99
|
+
...(finding.confidence === null ? [] : [field('Confidence', confidenceOf(finding.confidence))]),
|
|
100
|
+
field('Fingerprint', finding.fingerprint),
|
|
101
|
+
];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The card's `Introduced by` (§10 Terminal wording), for bundled-dependency code the scan found: the introducers, unless
|
|
105
|
+
* every one is the containing package itself, a direct dependency; `unknown (no lockfile)`, or the recorded reason,
|
|
106
|
+
* when they are unknown.
|
|
107
|
+
*/
|
|
108
|
+
function introducedByLine(report, finding) {
|
|
109
|
+
if (finding.attribution.provenance !== 'bundled-dependency' || finding.occurrences.length === 0)
|
|
110
|
+
return [];
|
|
111
|
+
const { introducedBy } = finding;
|
|
112
|
+
if (introducedBy !== undefined) {
|
|
113
|
+
const own = finding.attribution.package;
|
|
114
|
+
const itself = introducedBy.every((each) => each.slice(0, each.lastIndexOf('@')) === own);
|
|
115
|
+
// Yarn's introducers are capped at medium (§10 Formats).
|
|
116
|
+
const capped = report.completeness.attribution.introducers.confidence === 'medium' ? ' (medium)' : '';
|
|
117
|
+
return itself ? [] : [field('Introduced by', `${introducedBy.join(', ')}${capped}`)];
|
|
118
|
+
}
|
|
119
|
+
const { reason } = report.completeness.attribution.introducers;
|
|
120
|
+
const why = reason === null ? '' : ` (${reason === 'no-lockfile' ? 'no lockfile' : reason})`;
|
|
121
|
+
return [field('Introduced by', `unknown${why}`)];
|
|
122
|
+
}
|
|
123
|
+
function field(label, value) {
|
|
124
|
+
return ` ${label.padEnd(16)} ${value}`;
|
|
125
|
+
}
|
|
126
|
+
/** How the card writes each provenance (§10 Provenance). */
|
|
127
|
+
const provenanceLabels = {
|
|
128
|
+
'first-party': 'first-party',
|
|
129
|
+
'bundled-dependency': 'bundled dependency',
|
|
130
|
+
remote: 'remote',
|
|
131
|
+
inline: 'inline',
|
|
132
|
+
unknown: 'unknown',
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* The card's Package: the containing package's name, `unknown` when the code is bundled but no package is known, or
|
|
136
|
+
* `none` with the provenance for code that has no package (§10).
|
|
137
|
+
*/
|
|
138
|
+
function packageOf({ attribution }) {
|
|
139
|
+
if (attribution.package !== undefined)
|
|
140
|
+
return attribution.package;
|
|
141
|
+
if (attribution.provenance === 'unknown' || attribution.provenance === 'bundled-dependency')
|
|
142
|
+
return 'unknown';
|
|
143
|
+
return `none (${provenanceLabels[attribution.provenance]})`;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The card's Package with its version (§10 Terminal wording): `name@version` for the strong version in `packages`;
|
|
147
|
+
* every strong version the scan gives the package when it has several, counted over all the findings keyed on it,
|
|
148
|
+
* as `packages` counts them; else the finding's weak version with its source; else the name alone. Vendored code
|
|
149
|
+
* reads `tiny-helper (vendored in telemetry-agent@x)` with no version of its own: a vendored copy never gives one, and
|
|
150
|
+
* a version in `packages` would be another, installed copy's.
|
|
151
|
+
*/
|
|
152
|
+
function packageLine(report, finding) {
|
|
153
|
+
const name = packageOf(finding);
|
|
154
|
+
const { vendoredBy } = finding.attribution;
|
|
155
|
+
if (vendoredBy !== undefined)
|
|
156
|
+
return `${name} (vendored in ${vendoredBy})`;
|
|
157
|
+
if (finding.attribution.package === undefined)
|
|
158
|
+
return name;
|
|
159
|
+
const known = report.packages[name];
|
|
160
|
+
if (known !== undefined)
|
|
161
|
+
return `${name}@${known.version}`;
|
|
162
|
+
const strongVersions = [
|
|
163
|
+
...new Set(report.findings
|
|
164
|
+
.filter(({ attribution }) => attribution.package === name)
|
|
165
|
+
.flatMap(({ occurrences }) => occurrences)
|
|
166
|
+
.flatMap(({ evidence }) => evidence.version !== undefined && strongVersionSources.has(evidence.version.versionSource)
|
|
167
|
+
? [evidence.version.version]
|
|
168
|
+
: [])),
|
|
169
|
+
].sort(compareStrings);
|
|
170
|
+
if (strongVersions.length > 1)
|
|
171
|
+
return `${name} (versions ${strongVersions.join(', ')})`;
|
|
172
|
+
const weak = finding.occurrences.find(({ evidence }) => evidence.version !== undefined)?.evidence.version;
|
|
173
|
+
if (strongVersions.length === 0 && weak !== undefined)
|
|
174
|
+
return `${name} (${weak.version} from ${weak.versionSource}, a weak signal)`;
|
|
175
|
+
return name;
|
|
176
|
+
}
|
|
177
|
+
/** An attribution key as a change line shows it: a package key by its package name. */
|
|
178
|
+
function keyLabel(key) {
|
|
179
|
+
return key.startsWith('pkg:') ? key.slice('pkg:'.length) : key;
|
|
180
|
+
}
|
|
181
|
+
function changedReason(reason) {
|
|
182
|
+
if (reason.reason === 'attribution')
|
|
183
|
+
return `attribution ${keyLabel(reason.before)} → ${keyLabel(reason.after)}`;
|
|
184
|
+
if (reason.reason === 'locks')
|
|
185
|
+
return `locks ${reason.before ? 'yes' : 'no'} → ${reason.after ? 'yes' : 'no'}`;
|
|
186
|
+
return `effect ${reason.before} → ${reason.after}`;
|
|
187
|
+
}
|
|
188
|
+
/** A `removed` or `unconfirmed` entry: its target, effect and attribution key. */
|
|
189
|
+
function entryLine(finding) {
|
|
190
|
+
return ` ${targetName(finding).padEnd(targetWidth)} ${finding.effect} ${finding.attribution.key}`;
|
|
191
|
+
}
|
|
192
|
+
function noteLine(finding, note) {
|
|
193
|
+
const name = targetName(finding).padEnd(targetWidth);
|
|
194
|
+
if (note.kind === 'rekeyed')
|
|
195
|
+
return ` ${name} rekeyed ${note.before.join(', ')} → ${note.after}`;
|
|
196
|
+
if (note.kind === 'previous-version')
|
|
197
|
+
return ` ${name} ${note.package} ${note.before} → ${note.after}`;
|
|
198
|
+
if (note.kind === 'reachability-change')
|
|
199
|
+
return ` ${name} reachability ${note.before} → ${note.after}`;
|
|
200
|
+
if (note.kind === 'install-context-change')
|
|
201
|
+
return ` ${name} install context ${note.before} → ${note.after}`;
|
|
202
|
+
return ` ${name} shared target key: not paired with ${note.fingerprints.join(', ')}`;
|
|
203
|
+
}
|
|
204
|
+
/** A version-skew note (§4) as one line: the versions on each side, and what the skew means for the comparison. */
|
|
205
|
+
function skewLine(note) {
|
|
206
|
+
const side = (which) => `monkeypatchlist ${note.monkeypatchlistVersion[which]} · catalog ${note.catalogVersion[which]} · format ` +
|
|
207
|
+
`${note.formatVersion[which]} · fingerprint scheme ${note.fingerprintScheme[which]}`;
|
|
208
|
+
const meaning = note.newer === 'baseline'
|
|
209
|
+
? 'The baseline is newer, so no delta carries explainedBy'
|
|
210
|
+
: 'A delta without explainedBy may come from analyzer changes';
|
|
211
|
+
return ` Baseline written by ${side('baseline')} (this scan: ${side('scan')}). ${meaning}`;
|
|
212
|
+
}
|
|
213
|
+
/** An occurrence's place: its original location when a map resolves one, then its generated location. */
|
|
214
|
+
function sourceOf(report, occurrence) {
|
|
215
|
+
const path = report.artifacts.find((artifact) => artifact.url === occurrence.artifact.url)?.path;
|
|
216
|
+
const generated = `${path ?? occurrence.artifact.url}:${occurrence.generated.line}:${occurrence.generated.column}`;
|
|
217
|
+
const { original } = occurrence;
|
|
218
|
+
return original === null ? generated : `${original.source}:${original.line} (${generated})`;
|
|
219
|
+
}
|
|
220
|
+
/** Each confidence dimension's level, with its reasons when it is below `high` (§9). */
|
|
221
|
+
function confidenceOf(confidence) {
|
|
222
|
+
return ['target', 'mutation', 'attribution']
|
|
223
|
+
.map((dimension) => {
|
|
224
|
+
const { level, reasons } = confidence[dimension];
|
|
225
|
+
return `${dimension} ${level}${reasons.length === 0 ? '' : ` (${reasons.join(', ')})`}`;
|
|
226
|
+
})
|
|
227
|
+
.join(' · ');
|
|
228
|
+
}
|
|
229
|
+
/** Nonzero counts as `name n`, joined. */
|
|
230
|
+
function counts(entries) {
|
|
231
|
+
return entries
|
|
232
|
+
.filter(([, n]) => n > 0)
|
|
233
|
+
.map(([name, n]) => `${name} ${n}`)
|
|
234
|
+
.join(' · ');
|
|
235
|
+
}
|
|
236
|
+
/** The completeness block (§13): the level, the artifact counters and the limitation kinds. */
|
|
237
|
+
function completeness(report, paint) {
|
|
238
|
+
const { level, counters, limitations } = report.completeness;
|
|
239
|
+
const { artifacts, statistics } = counters;
|
|
240
|
+
const style = level === 'complete' ? 'green' : level === 'incomplete' ? 'red' : 'yellow';
|
|
241
|
+
const sum = (record) => Object.values(record).reduce((a, b) => a + b, 0);
|
|
242
|
+
const breakdown = (record) => {
|
|
243
|
+
const listed = counts(Object.entries(record));
|
|
244
|
+
return listed === '' ? '' : ` (${listed})`;
|
|
245
|
+
};
|
|
246
|
+
const lines = [
|
|
247
|
+
`Completeness ${paint(style, level)}`,
|
|
248
|
+
` Known ${artifacts.known} · analyzed ${artifacts.analyzed} · failed ${sum(artifacts.failed)}${breakdown(artifacts.failed)}` +
|
|
249
|
+
` · skipped ${sum(artifacts.skipped)}${breakdown(artifacts.skipped)}` +
|
|
250
|
+
(artifacts.sweepOnly > 0 ? ` · sweep-only ${artifacts.sweepOnly}` : ''),
|
|
251
|
+
];
|
|
252
|
+
const kinds = counts(limitationKinds.map((kind) => [kind, limitations.filter((record) => record.kind === kind).length]));
|
|
253
|
+
if (kinds !== '')
|
|
254
|
+
lines.push(` Limitations ${kinds}`);
|
|
255
|
+
if (statistics.budgetExhausted > 0) {
|
|
256
|
+
const groups = statistics.budgetExhausted;
|
|
257
|
+
lines.push(` Step budget exhausted in ${groups} link ${groups === 1 ? 'group' : 'groups'}`);
|
|
258
|
+
}
|
|
259
|
+
if (statistics.crashGuard === 'fired')
|
|
260
|
+
lines.push(' Crash guard fired: the scan reports no findings');
|
|
261
|
+
return lines;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The Inputs block (§15 Terminal): what each missing input costs, following the input tiers (§2). Without a source map,
|
|
265
|
+
* bundled code has no containing package and no original locations; without a lockfile, there are no lockfile versions
|
|
266
|
+
* and no introducing dependencies.
|
|
267
|
+
*/
|
|
268
|
+
function inputs(report) {
|
|
269
|
+
const { withMap, withoutMap, withBrokenMap } = report.completeness.attribution.artifacts;
|
|
270
|
+
const total = withMap + withoutMap + withBrokenMap;
|
|
271
|
+
let maps = `${withMap} / ${total} artifacts`;
|
|
272
|
+
if (withBrokenMap > 0)
|
|
273
|
+
maps += ` · ${withBrokenMap} with broken maps`;
|
|
274
|
+
if (withMap < total)
|
|
275
|
+
maps += ' · without a map: Package unknown for bundled code, no original locations';
|
|
276
|
+
// A mapped artifact can still leave lines unmapped, so the findings with no package are counted either way: the
|
|
277
|
+
// current findings whose card reads `Package unknown`.
|
|
278
|
+
const unknown = report.findings.filter((finding) => finding.occurrences.length > 0 && packageOf(finding) === 'unknown').length;
|
|
279
|
+
if (unknown > 0)
|
|
280
|
+
maps += ` · ${unknown} ${unknown === 1 ? 'finding has' : 'findings have'} Package unknown`;
|
|
281
|
+
return ['Inputs', ` Source maps ${maps}`, ` Lockfile ${lockfile(report)}`];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The Inputs block's Lockfile (§10, §15): the lockfile as given, and when introducers are unknown, why, what that
|
|
285
|
+
* costs, and the evidence.
|
|
286
|
+
*/
|
|
287
|
+
function lockfile(report) {
|
|
288
|
+
const given = report.config.lockfile;
|
|
289
|
+
if (given === null)
|
|
290
|
+
return 'not given · no lockfile versions; Introduced by unknown';
|
|
291
|
+
const { reason, evidence, confidence } = report.completeness.attribution.introducers;
|
|
292
|
+
if (reason === null)
|
|
293
|
+
return confidence === 'medium' ? `${given.path} · Introduced by medium` : given.path;
|
|
294
|
+
const why = `${reason}${evidence === null ? '' : `: ${evidence}`}`;
|
|
295
|
+
const cost = reason === 'importer-unresolved' ? 'Introduced by unknown' : 'no lockfile versions; Introduced by unknown';
|
|
296
|
+
return `${given.path} · ${why} · ${cost}`;
|
|
297
|
+
}
|
|
298
|
+
/** How many findings a `protected` scan left out, per surface (§12), with the flag that shows them. */
|
|
299
|
+
function outsideScope({ summary }) {
|
|
300
|
+
const listed = counts(surfaces.map((surface) => [surface, summary.outOfScopeBySurface[surface]]));
|
|
301
|
+
return listed === '' ? 'Outside scope none' : `Outside scope ${listed} (run with --surface all)`;
|
|
302
|
+
}
|
|
303
|
+
/** The ineffective writes (§3): a count, or under `--list-ineffective` a line per record and occurrence. */
|
|
304
|
+
function ineffective(report, list) {
|
|
305
|
+
const count = report.summary.ineffectiveWrites;
|
|
306
|
+
if (count === 0)
|
|
307
|
+
return ['Ineffective writes 0'];
|
|
308
|
+
if (!list)
|
|
309
|
+
return [`Ineffective writes ${count} (--list-ineffective)`];
|
|
310
|
+
return [
|
|
311
|
+
`Ineffective writes ${count}`,
|
|
312
|
+
...report.ineffectiveWrites.flatMap((record) => [
|
|
313
|
+
` ${targetName(record).padEnd(targetWidth)} ${record.reason}`,
|
|
314
|
+
...record.occurrences.map((occurrence) => ` ${occurrence.effect} · ${occurrence.mechanism} · throws ${occurrence.throws} · ` +
|
|
315
|
+
`${sourceOf(report, occurrence)}`),
|
|
316
|
+
]),
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
/** The gate line (§15 Terminal): the states it fails on, its result and the exit code. */
|
|
320
|
+
function gateLine(report, paint) {
|
|
321
|
+
const { gate } = report;
|
|
322
|
+
if (gate.result === 'not-run')
|
|
323
|
+
return 'Gate not run (no --fail-on)';
|
|
324
|
+
const failed = report.findings.filter((finding) => finding.policy !== null).length;
|
|
325
|
+
const result = gate.result === 'fail'
|
|
326
|
+
? paint('red', `FAIL (${failed} ${failed === 1 ? 'finding' : 'findings'})`)
|
|
327
|
+
: paint('green', 'pass');
|
|
328
|
+
// Exit code 3 is the scan's, not the findings' (§16): the line says why.
|
|
329
|
+
const incomplete = gate.exitCode === 3 ? ' · scan incomplete' : '';
|
|
330
|
+
return `Gate fail-on ${gate.failOn.join(',')} → ${result}${incomplete} exit ${gate.exitCode}`;
|
|
331
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Fetcher } from './index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* A recorded HTTP response: its status (200 when left out), its headers (names in any case) and its body. A 3xx
|
|
4
|
+
* response with a `Location` header redirects to that URL, resolved against the one requested.
|
|
5
|
+
*/
|
|
6
|
+
export interface RecordedResponse {
|
|
7
|
+
status?: number;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
body?: string | Uint8Array;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A fetcher that serves `responses`, by URL. A fragment is never part of a request, so it is dropped before the lookup.
|
|
13
|
+
* It follows recorded redirects, and gives:
|
|
14
|
+
*
|
|
15
|
+
* - `ok`, with the bytes and the final response, for a 2xx response, or `sri-mismatch` when its bytes don't match the
|
|
16
|
+
* request's integrity value (§17 Integrity)
|
|
17
|
+
* - `fetch-failed` with the response, for any other status (§17 Outcomes)
|
|
18
|
+
* - `fetch-failed` with no response, for a URL with no recorded response, as for a host that doesn't answer
|
|
19
|
+
* - `remote-refused` with no response, for a sixth redirect, as the safety floor gives (§17 Safety floor). A redirect
|
|
20
|
+
* loop is refused so too: there is no separate loop check.
|
|
21
|
+
*/
|
|
22
|
+
export declare function fixtureFetcher(responses: Readonly<Record<string, RecordedResponse>>): Fetcher;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The fixture fetcher (§19 Source store and retrieval): serves recorded responses, for tests and build-time tools. It
|
|
2
|
+
// never touches the network.
|
|
3
|
+
import { verified } from "./lib/integrity.js";
|
|
4
|
+
import { defaultLimits } from "./resolve.js";
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
/**
|
|
7
|
+
* A fetcher that serves `responses`, by URL. A fragment is never part of a request, so it is dropped before the lookup.
|
|
8
|
+
* It follows recorded redirects, and gives:
|
|
9
|
+
*
|
|
10
|
+
* - `ok`, with the bytes and the final response, for a 2xx response, or `sri-mismatch` when its bytes don't match the
|
|
11
|
+
* request's integrity value (§17 Integrity)
|
|
12
|
+
* - `fetch-failed` with the response, for any other status (§17 Outcomes)
|
|
13
|
+
* - `fetch-failed` with no response, for a URL with no recorded response, as for a host that doesn't answer
|
|
14
|
+
* - `remote-refused` with no response, for a sixth redirect, as the safety floor gives (§17 Safety floor). A redirect
|
|
15
|
+
* loop is refused so too: there is no separate loop check.
|
|
16
|
+
*/
|
|
17
|
+
export function fixtureFetcher(responses) {
|
|
18
|
+
const recorded = new Map(Object.entries(responses).map(([url, response]) => [withoutFragment(url), response]));
|
|
19
|
+
return {
|
|
20
|
+
async fetch(url, { integrity = '' }) {
|
|
21
|
+
const { redirects: most } = defaultLimits;
|
|
22
|
+
for (let current = withoutFragment(url), redirects = 0;; redirects++) {
|
|
23
|
+
const response = recorded.get(current);
|
|
24
|
+
if (response === undefined) {
|
|
25
|
+
return { status: 'fetch-failed', detail: `no recorded response for ${current}`, response: null };
|
|
26
|
+
}
|
|
27
|
+
const status = response.status ?? 200;
|
|
28
|
+
const headers = new Map(Object.entries(response.headers ?? {}).map(([name, value]) => [name.toLowerCase(), value]));
|
|
29
|
+
const location = headers.get('location');
|
|
30
|
+
if (status >= 300 && status < 400 && location !== undefined) {
|
|
31
|
+
if (redirects === most) {
|
|
32
|
+
const detail = `the safety floor follows at most ${most} redirects`;
|
|
33
|
+
return { status: 'remote-refused', detail, response: null };
|
|
34
|
+
}
|
|
35
|
+
current = withoutFragment(new URL(location, current).href);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const fetched = {
|
|
39
|
+
finalUrl: current,
|
|
40
|
+
contentType: headers.get('content-type') ?? null,
|
|
41
|
+
etag: headers.get('etag') ?? null,
|
|
42
|
+
lastModified: headers.get('last-modified') ?? null,
|
|
43
|
+
sourceMap: headers.get('sourcemap') ?? headers.get('x-sourcemap') ?? null,
|
|
44
|
+
};
|
|
45
|
+
if (status < 200 || status > 299)
|
|
46
|
+
return { status: 'fetch-failed', detail: `HTTP ${status}`, response: fetched };
|
|
47
|
+
const body = response.body ?? '';
|
|
48
|
+
return verified(typeof body === 'string' ? encoder.encode(body) : body, fetched, integrity);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The URL without its fragment. */
|
|
54
|
+
function withoutFragment(url) {
|
|
55
|
+
const parsed = new URL(url);
|
|
56
|
+
parsed.hash = '';
|
|
57
|
+
return parsed.href;
|
|
58
|
+
}
|