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
package/dist/scan.js
ADDED
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
// The scan pipeline (§19): config → source store → discovery → analysis → attribution → findings → compare → policy →
|
|
2
|
+
// report. Only the source store reads bytes; every module after it is pure.
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { realms } from "./catalog.js";
|
|
5
|
+
import { chunkRegistries, lexicalCollisions, } from "./packages/analysis/index.js";
|
|
6
|
+
import { groupJob } from "./packages/analysis/job.js";
|
|
7
|
+
import { attribution } from "./packages/attribution/index.js";
|
|
8
|
+
import { compare, withCatalogStatus, withExplainedBy } from "./packages/compare/index.js";
|
|
9
|
+
import { aggregate } from "./packages/completeness/index.js";
|
|
10
|
+
import { discover } from "./packages/discovery/index.js";
|
|
11
|
+
import { applySurface, buildFindings, buildIneffectiveWrites, compareStrings, fingerprintScheme, packageVersions, } from "./packages/findings/index.js";
|
|
12
|
+
import { inProcessRunner } from "./packages/parallel/index.js";
|
|
13
|
+
import { applyPolicy } from "./packages/policy/index.js";
|
|
14
|
+
import { onceStore } from "./packages/source-store/index.js";
|
|
15
|
+
/** The order of an artifact's two source types, wherever a URL run both ways lists them. */
|
|
16
|
+
const sourceTypes = ['script', 'module'];
|
|
17
|
+
/**
|
|
18
|
+
* Runs a scan, and returns everything the report needs except the `run` block.
|
|
19
|
+
*
|
|
20
|
+
* The scan runs in rounds (§19 References only the resolver can see): discover, analyze the link groups, take the
|
|
21
|
+
* references only the resolver can see, and discover again with them, until a round's groups find no reference the
|
|
22
|
+
* round didn't start from. Each round discovers and analyzes the whole artifact graph its references give, so the
|
|
23
|
+
* result is the last round's: analyzing, over the final artifact set, every referenced entry's group, then each swept
|
|
24
|
+
* artifact no group links, whatever rounds led there (The result doesn't depend on the rounds). A group whose job is
|
|
25
|
+
* the same as in an earlier round isn't analyzed again: the resolver is a pure function of its job. `seed` gives the
|
|
26
|
+
* first round's references.
|
|
27
|
+
*/
|
|
28
|
+
export async function scan(config, deps, seed = []) {
|
|
29
|
+
const store = onceStore(deps.store);
|
|
30
|
+
const memo = new Map();
|
|
31
|
+
const known = new Map(seed.map((reference) => [JSON.stringify(reference), reference]));
|
|
32
|
+
let result;
|
|
33
|
+
for (;;) {
|
|
34
|
+
const references = [...known.values()].sort(byReferenceOrder);
|
|
35
|
+
result = await round(config, deps, store, references, memo);
|
|
36
|
+
const fresh = result.references.filter((reference) => !known.has(JSON.stringify(reference)));
|
|
37
|
+
if (result.fired || fresh.length === 0)
|
|
38
|
+
break;
|
|
39
|
+
for (const reference of fresh)
|
|
40
|
+
known.set(JSON.stringify(reference), reference);
|
|
41
|
+
}
|
|
42
|
+
return finish(config, deps, result);
|
|
43
|
+
}
|
|
44
|
+
/** The order rounds process references in: by URL, then by the artifact they are in, its realm and where. */
|
|
45
|
+
function byReferenceOrder(a, b) {
|
|
46
|
+
return (compareStrings(a.url, b.url) ||
|
|
47
|
+
compareStrings(a.from.url, b.from.url) ||
|
|
48
|
+
compareStrings(a.from.fetched ?? '', b.from.fetched ?? '') ||
|
|
49
|
+
realms.indexOf(a.realm) - realms.indexOf(b.realm) ||
|
|
50
|
+
a.location.start - b.location.start ||
|
|
51
|
+
compareStrings(a.kind, b.kind) ||
|
|
52
|
+
compareStrings(a.load ?? '', b.load ?? '') ||
|
|
53
|
+
Number(a.partial === true) - Number(b.partial === true) ||
|
|
54
|
+
Number(a.none === true) - Number(b.none === true) ||
|
|
55
|
+
(a.alternative?.start ?? -1) - (b.alternative?.start ?? -1) ||
|
|
56
|
+
compareStrings(JSON.stringify(a.type ?? ''), JSON.stringify(b.type ?? '')) ||
|
|
57
|
+
compareStrings(a.integrity ?? '', b.integrity ?? '') ||
|
|
58
|
+
compareStrings(JSON.stringify(a.integrityAlternatives ?? ''), JSON.stringify(b.integrityAlternatives ?? '')) ||
|
|
59
|
+
compareStrings(a.noModule ?? '', b.noModule ?? ''));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* One round (`scan`): discovers the artifact graph with `references`, and analyzes its link groups, taking a group's
|
|
63
|
+
* analysis from `analyses` when an earlier round ran the same job, and adding each it runs.
|
|
64
|
+
*/
|
|
65
|
+
async function round(config, deps, store, references, memo) {
|
|
66
|
+
const { catalog, resolver } = deps;
|
|
67
|
+
const discovered = await discover({ entries: config.entries, store, catalog, sourceMaps: config.sourceMaps, references });
|
|
68
|
+
const { artifacts } = discovered;
|
|
69
|
+
const sources = new Map(realms.map((realm) => [realm, new Map()]));
|
|
70
|
+
// The modules an import can name, by realm and artifact id (`artifactId`): a URL run both ways is imported as its
|
|
71
|
+
// module.
|
|
72
|
+
const modules = new Map(realms.map((realm) => [realm, new Map()]));
|
|
73
|
+
const analyzable = [];
|
|
74
|
+
for (const artifact of artifacts) {
|
|
75
|
+
const { url, hash, sourceType, realm, realmBasis, text, runs, discovery, origin, parent } = artifact;
|
|
76
|
+
if (hash === null || text === null)
|
|
77
|
+
continue;
|
|
78
|
+
const fetched = artifact.retrieval.requestedUrl;
|
|
79
|
+
const source = {
|
|
80
|
+
...{ id: artifactId(artifact), url, hash, sourceType, realm, realmBasis, text, runs, discovery, origin, parent },
|
|
81
|
+
...(fetched === null || fetched === url ? {} : { fetched }),
|
|
82
|
+
};
|
|
83
|
+
analyzable.push(source);
|
|
84
|
+
const inRealm = sources.get(realm);
|
|
85
|
+
if (!inRealm.has(url))
|
|
86
|
+
inRealm.set(url, source);
|
|
87
|
+
const imported = modules.get(realm);
|
|
88
|
+
if (imported.get(source.id)?.sourceType !== 'module')
|
|
89
|
+
imported.set(source.id, source);
|
|
90
|
+
}
|
|
91
|
+
// How discovery resolved each script's module requests (§6 Module links), by realm and entry document, whose import
|
|
92
|
+
// maps resolve them, and as first resolved anywhere: a module that an unlinked swept file imports joins that file's
|
|
93
|
+
// group in its assumed window realm (§19), so its own imports resolve as a reference elsewhere resolved them.
|
|
94
|
+
// Scripts and modules are named by their artifact ids.
|
|
95
|
+
const resolved = new Map();
|
|
96
|
+
const resolvedAnywhere = new Map();
|
|
97
|
+
const requestsFrom = new Map();
|
|
98
|
+
for (const { realm, document, from, specifier, to } of discovered.imports) {
|
|
99
|
+
const importer = idOf(from);
|
|
100
|
+
resolved.set(JSON.stringify([realm, document, importer, specifier]), idOf(to));
|
|
101
|
+
if (!resolvedAnywhere.has(JSON.stringify([importer, specifier]))) {
|
|
102
|
+
resolvedAnywhere.set(JSON.stringify([importer, specifier]), idOf(to));
|
|
103
|
+
const list = requestsFrom.get(importer);
|
|
104
|
+
if (list === undefined)
|
|
105
|
+
requestsFrom.set(importer, [specifier]);
|
|
106
|
+
else
|
|
107
|
+
list.push(specifier);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const resolveIn = (realm, document) => (from, specifier) => resolved.get(JSON.stringify([realm, document, from, specifier])) ?? resolvedAnywhere.get(JSON.stringify([from, specifier]));
|
|
111
|
+
// The entry documents that run each artifact with references other than imports, each with its own import maps.
|
|
112
|
+
const documents = new Map(discovered.documents.map(({ realm, url, documents }) => [JSON.stringify([realm, url]), documents]));
|
|
113
|
+
const documentsOf = ({ realm, url }) => documents.get(JSON.stringify([realm, url])) ?? [null];
|
|
114
|
+
/** The module with id `id` an import in a group of `realm` names: the realm's own, or else another realm's. */
|
|
115
|
+
const moduleIn = (realm, id) => {
|
|
116
|
+
const own = modules.get(realm).get(id);
|
|
117
|
+
if (own !== undefined)
|
|
118
|
+
return own;
|
|
119
|
+
for (const other of realms) {
|
|
120
|
+
const found = modules.get(other).get(id);
|
|
121
|
+
if (found !== undefined)
|
|
122
|
+
return found;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
};
|
|
126
|
+
// The chunk registries each artifact's code pushes into and reads (§6 idiom 6), by artifact id, and the
|
|
127
|
+
// build-directory artifacts that push each chunk id into each registry, by registry and id ('' for every id), in URL
|
|
128
|
+
// order. A chunk is identified by its registry's name and its id, never by its URL.
|
|
129
|
+
const registries = new Map();
|
|
130
|
+
const registryOf = (source) => {
|
|
131
|
+
let found = registries.get(source.id);
|
|
132
|
+
if (found === undefined)
|
|
133
|
+
registries.set(source.id, (found = chunkRegistries(source.text, source.sourceType)));
|
|
134
|
+
return found;
|
|
135
|
+
};
|
|
136
|
+
const pushers = new Map();
|
|
137
|
+
for (const source of [...analyzable].sort((a, b) => compareStrings(a.url, b.url) || compareStrings(a.id, b.id))) {
|
|
138
|
+
if (source.origin !== 'local-build')
|
|
139
|
+
continue;
|
|
140
|
+
for (const { registry, chunks } of registryOf(source).pushes) {
|
|
141
|
+
for (const id of [...chunks, '']) {
|
|
142
|
+
const key = JSON.stringify([registry, id]);
|
|
143
|
+
const list = pushers.get(key);
|
|
144
|
+
if (list === undefined)
|
|
145
|
+
pushers.set(key, new Set([source.id]));
|
|
146
|
+
else
|
|
147
|
+
list.add(source.id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const chunksOf = (registry, id) => [...(pushers.get(JSON.stringify([registry, id ?? ''])) ?? [])];
|
|
152
|
+
// The remote chunks each loader's folded chunk URLs name (§2, §19), by realm and loader id: a remote chunk is no
|
|
153
|
+
// build-directory pusher, so it joins only the groups of a loader that loads it.
|
|
154
|
+
const remoteChunks = new Map();
|
|
155
|
+
for (const { realm, loader, chunk } of discovered.remoteChunks) {
|
|
156
|
+
const source = modules.get(realm).get(chunk);
|
|
157
|
+
if (source === undefined)
|
|
158
|
+
continue;
|
|
159
|
+
const key = JSON.stringify([realm, idOf(loader)]);
|
|
160
|
+
const list = remoteChunks.get(key);
|
|
161
|
+
if (list === undefined)
|
|
162
|
+
remoteChunks.set(key, [source]);
|
|
163
|
+
else if (!list.includes(source))
|
|
164
|
+
list.push(source);
|
|
165
|
+
}
|
|
166
|
+
/** The remote chunks `loader` loads that push the chunk id `id` (null: any) into `registry`, by artifact id. */
|
|
167
|
+
const remoteChunksOf = (loader, registry, id) => (remoteChunks.get(JSON.stringify([loader.realm, loader.id])) ?? [])
|
|
168
|
+
.filter((chunk) => registryOf(chunk).pushes.some((push) => push.registry === registry && (id === null || push.chunks.includes(id))))
|
|
169
|
+
.map((chunk) => chunk.id);
|
|
170
|
+
/**
|
|
171
|
+
* The chunks a source's loaders load: the build-directory pushers of each id their id tables map, and the remote
|
|
172
|
+
* chunks the source loads that push one, by artifact id.
|
|
173
|
+
*/
|
|
174
|
+
const loadedChunks = (source) => registryOf(source).loads.flatMap(({ registry, ids }) => (ids === null ? [null] : ids).flatMap((id) => [...chunksOf(registry, id), ...remoteChunksOf(source, registry, id)]));
|
|
175
|
+
// The classic scripts each entry document's script elements run (§6), in URL order: a page's initial chunks are
|
|
176
|
+
// those that push into a registry a loader among them reads.
|
|
177
|
+
const pageScripts = new Map();
|
|
178
|
+
for (const source of [...analyzable].sort((a, b) => compareStrings(a.url, b.url) || compareStrings(a.id, b.id))) {
|
|
179
|
+
if (source.realm !== 'window' || !source.runs || source.sourceType !== 'script' || source.parent === null)
|
|
180
|
+
continue;
|
|
181
|
+
if (!documentsOf(source).includes(source.parent))
|
|
182
|
+
continue;
|
|
183
|
+
for (const document of documentsOf(source)) {
|
|
184
|
+
const list = pageScripts.get(document ?? '');
|
|
185
|
+
if (list === undefined)
|
|
186
|
+
pageScripts.set(document ?? '', [source]);
|
|
187
|
+
else
|
|
188
|
+
list.push(source);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** The other scripts of `document` that push into `registry`: the initial chunks of a loader there. */
|
|
192
|
+
const initialChunksOf = (entry, document, registry) => (pageScripts.get(document ?? '') ?? []).filter((script) => script !== entry && registryOf(script).pushes.some((push) => push.registry === registry));
|
|
193
|
+
/** The loaders among a document's scripts, by the registry they read: each registry's first loader in URL order. */
|
|
194
|
+
const pageLoaders = (document) => {
|
|
195
|
+
const loaders = new Map();
|
|
196
|
+
for (const script of pageScripts.get(document ?? '') ?? []) {
|
|
197
|
+
for (const { registry } of registryOf(script).loads)
|
|
198
|
+
if (!loaders.has(registry))
|
|
199
|
+
loaders.set(registry, script);
|
|
200
|
+
}
|
|
201
|
+
return loaders;
|
|
202
|
+
};
|
|
203
|
+
// The scripts a page's loader links as initial chunks, which are never analyzed on their own (§19).
|
|
204
|
+
const initialChunks = new Set();
|
|
205
|
+
for (const key of pageScripts.keys()) {
|
|
206
|
+
const document = key === '' ? null : key;
|
|
207
|
+
const loaders = pageLoaders(document);
|
|
208
|
+
for (const [registry, loader] of loaders) {
|
|
209
|
+
for (const chunk of initialChunksOf(loader, document, registry))
|
|
210
|
+
if (loaders.get(registry) !== chunk)
|
|
211
|
+
initialChunks.add(chunk);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The members a group's imports and chunk loaders reach from `entry`, as discovery resolved the imports, the entry
|
|
216
|
+
* first (§6 Link groups).
|
|
217
|
+
*/
|
|
218
|
+
const linkedFrom = (entry, document = documentsOf(entry)[0]) => {
|
|
219
|
+
const seen = new Set([entry.id]);
|
|
220
|
+
const queue = [entry];
|
|
221
|
+
for (let next = 0; next < queue.length; next++) {
|
|
222
|
+
const from = queue[next];
|
|
223
|
+
const imported = (requestsFrom.get(from.id) ?? []).map((specifier) => resolveIn(entry.realm, document)(from.id, specifier));
|
|
224
|
+
const initial = registryOf(from).loads.flatMap(({ registry }) => pageLoaders(document).get(registry) === entry ? initialChunksOf(entry, document, registry).map((chunk) => chunk.id) : []);
|
|
225
|
+
for (const id of [...imported, ...initial, ...loadedChunks(from)]) {
|
|
226
|
+
const member = id === undefined || seen.has(id) ? undefined : moduleIn(entry.realm, id);
|
|
227
|
+
if (member === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
seen.add(member.id);
|
|
230
|
+
queue.push(member);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return queue;
|
|
234
|
+
};
|
|
235
|
+
// One group per analysis (§19 Link groups): first every artifact a reference other than an import runs, then each
|
|
236
|
+
// one that only a preload names, then each one only the sweep found, each in URL order, a URL in several realms in
|
|
237
|
+
// the order of `realms`, and one run both ways first as a classic script. A module only imports name is analyzed
|
|
238
|
+
// only in its importers' groups. A preload-only or sweep-only artifact that a group links is never also analyzed on
|
|
239
|
+
// its own, and of those that link each other, the first in group order is the entry of their group.
|
|
240
|
+
const importedOnly = new Set(discovered.importedOnly.map(({ realm, url }) => JSON.stringify([realm, url])));
|
|
241
|
+
const order = ({ runs, discovery }) => (runs ? 0 : discovery !== 'sweep' ? 1 : 2);
|
|
242
|
+
const byGroupOrder = (a, b) => order(a) - order(b) ||
|
|
243
|
+
compareStrings(a.url, b.url) ||
|
|
244
|
+
compareStrings(a.id, b.id) ||
|
|
245
|
+
realms.indexOf(a.realm) - realms.indexOf(b.realm) ||
|
|
246
|
+
sourceTypes.indexOf(a.sourceType) - sourceTypes.indexOf(b.sourceType);
|
|
247
|
+
const candidates = analyzable
|
|
248
|
+
.filter((source) => !(source.sourceType === 'module' && importedOnly.has(JSON.stringify([source.realm, source.url]))))
|
|
249
|
+
.sort(byGroupOrder);
|
|
250
|
+
// An HTML-referenced script another entry's group links through a chunk registry is no group of its own.
|
|
251
|
+
const entries = candidates.filter((source) => source.runs && !initialChunks.has(source));
|
|
252
|
+
const linked = new Set(entries.flatMap((entry) => documentsOf(entry).flatMap((document) => linkedFrom(entry, document).slice(1))));
|
|
253
|
+
const unlinked = candidates.filter((source) => !source.runs && !linked.has(source));
|
|
254
|
+
const reached = new Map(unlinked.map((source) => [source, new Set(linkedFrom(source).slice(1))]));
|
|
255
|
+
const groupEntries = [
|
|
256
|
+
...entries,
|
|
257
|
+
...unlinked.filter((source) => unlinked.every((other) => other === source ||
|
|
258
|
+
!reached.get(other).has(source) ||
|
|
259
|
+
(reached.get(source).has(other) && byGroupOrder(source, other) < 0))),
|
|
260
|
+
];
|
|
261
|
+
const analyses = new Map();
|
|
262
|
+
const queue = [];
|
|
263
|
+
const plan = (sources, fresh = false) => {
|
|
264
|
+
for (const source of sources) {
|
|
265
|
+
for (const document of documentsOf(source)) {
|
|
266
|
+
const members = linkedFrom(source, document);
|
|
267
|
+
const identity = [source.hash, source.sourceType, source.realm, source.realmBasis];
|
|
268
|
+
const linking = members.map(({ url }) => url);
|
|
269
|
+
const key = JSON.stringify(members.length > 1 ? [...identity, source.id, linking] : identity);
|
|
270
|
+
const analysis = fresh ? undefined : analyses.get(key);
|
|
271
|
+
if (analysis === undefined) {
|
|
272
|
+
const planned = { entry: source, copies: [source], members, document };
|
|
273
|
+
analyses.set(key, planned);
|
|
274
|
+
queue.push(planned);
|
|
275
|
+
}
|
|
276
|
+
else if (!analysis.copies.includes(source))
|
|
277
|
+
analysis.copies.push(source);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
plan(groupEntries);
|
|
282
|
+
// The artifacts a group was planned or ran for, as its entry, a copy of it, or a member, by realm, URL and source type.
|
|
283
|
+
const covered = new Set();
|
|
284
|
+
const cover = (realm, url, sourceType) => covered.add(JSON.stringify([realm, url, sourceType]));
|
|
285
|
+
const found = [];
|
|
286
|
+
// Every classic script's top-level `let`, `const` and `class` declarations, for the scan-wide lexical-name check (§3).
|
|
287
|
+
const declarations = [];
|
|
288
|
+
const ineffectiveWrites = [];
|
|
289
|
+
const limitations = [...discovered.limitations];
|
|
290
|
+
const statistics = [];
|
|
291
|
+
// Each analyzed artifact's module linking (§6), by its copy, realm and source type.
|
|
292
|
+
const linking = new Map();
|
|
293
|
+
// A URL run both as a classic script and as a module is analyzed both ways (§2 Files that don't run as referenced).
|
|
294
|
+
// A write both analyses find is one occurrence of it, at one place in one artifact.
|
|
295
|
+
const occurrenceAnalyses = new Map();
|
|
296
|
+
const ineffectiveAnalyses = new Map();
|
|
297
|
+
// The crash guard is asked before each group, never inside one. Once it fires, no further group runs, and the
|
|
298
|
+
// aggregator counts the artifacts it kept from being analyzed.
|
|
299
|
+
let fired = false;
|
|
300
|
+
const stopped = [];
|
|
301
|
+
/**
|
|
302
|
+
* Plans a group of its own for each artifact no group has covered once the planned groups have run, in group order:
|
|
303
|
+
* a module whose only importer is no group's member, such as one only a classic preload's syntax-typed visit
|
|
304
|
+
* imports. Whether any was planned.
|
|
305
|
+
*/
|
|
306
|
+
const leftovers = () => {
|
|
307
|
+
const left = analyzable
|
|
308
|
+
.filter((source) => !covered.has(JSON.stringify([source.realm, source.id, source.sourceType])))
|
|
309
|
+
.sort(byGroupOrder);
|
|
310
|
+
if (left.length === 0)
|
|
311
|
+
return false;
|
|
312
|
+
plan(left.slice(0, 1), true);
|
|
313
|
+
return true;
|
|
314
|
+
};
|
|
315
|
+
// A record of a member other than the entry is the member's own, whichever groups link it: it is kept once.
|
|
316
|
+
const memberRecords = new Set();
|
|
317
|
+
// The artifacts the groups that ran linked, by realm and URL, so that each is stopped at most once.
|
|
318
|
+
const reachedBy = new Set();
|
|
319
|
+
const groups = deps.groups ?? inProcessRunner(resolver, catalog, deps.budget);
|
|
320
|
+
/** The group a planned analysis runs, as plain data that can cross a worker boundary (§19 Serializable seams). */
|
|
321
|
+
const jobOf = ({ entry, document, members }) => {
|
|
322
|
+
const { url, hash, sourceType, realm, realmBasis } = entry;
|
|
323
|
+
const idOfName = (name) => (name === url ? entry.id : name);
|
|
324
|
+
return groupJob({
|
|
325
|
+
entry: { url, hash, sourceType, realm, realmBasis },
|
|
326
|
+
// The entry is the copy analyzed, whatever other way its URL runs. Every other member is a module an import names,
|
|
327
|
+
// named by its artifact id.
|
|
328
|
+
artifact: (member) => {
|
|
329
|
+
const source = member === url || member === entry.id ? entry : moduleIn(realm, member);
|
|
330
|
+
if (source === undefined)
|
|
331
|
+
return undefined;
|
|
332
|
+
const { hash, sourceType, realmBasis, text, origin } = source;
|
|
333
|
+
const named = source === entry ? url : source.id;
|
|
334
|
+
return { url: named, hash, sourceType, realm, realmBasis, text, ...(origin === undefined ? {} : { origin }) };
|
|
335
|
+
},
|
|
336
|
+
resolveImport: (from, specifier) => resolveIn(realm, document)(idOfName(from), specifier),
|
|
337
|
+
requestsOf: (from) => requestsFrom.get(idOfName(from)) ?? [],
|
|
338
|
+
chunks: (registry, id) => [...new Set([...chunksOf(registry, id), ...members.flatMap((member) => remoteChunksOf(member, registry, id))])].filter((chunk) => chunk !== entry.id),
|
|
339
|
+
initialChunks: (registry) => pageLoaders(document).get(registry) === entry ? initialChunksOf(entry, document, registry).map((chunk) => chunk.id) : [],
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
// The planned groups run a batch at a time: those planned so far, then those `leftovers` plans once they have run.
|
|
343
|
+
// Each batch's results merge in group order, whatever order they finish in (§18 Parallelism). The crash guard is
|
|
344
|
+
// asked before each group starts, in group order.
|
|
345
|
+
const results = [];
|
|
346
|
+
// The references only the resolver can see the groups found, once each.
|
|
347
|
+
const foundReferences = [];
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
for (let index = 0; index < queue.length || leftovers(); index++) {
|
|
350
|
+
if (index === results.length) {
|
|
351
|
+
const batch = queue.slice(index);
|
|
352
|
+
if (fired)
|
|
353
|
+
results.push(...batch.map(() => undefined));
|
|
354
|
+
else {
|
|
355
|
+
// A job an earlier round ran gives the same analysis (§19 Purity), so only the others run.
|
|
356
|
+
const jobs = batch.map(jobOf);
|
|
357
|
+
const keys = jobs.map((job) => createHash('sha256').update(JSON.stringify(job)).digest('hex'));
|
|
358
|
+
const fresh = jobs.filter((_, at) => !memo.has(keys[at]));
|
|
359
|
+
const ran = await groups.run(fresh, () => !(fired ||= deps.crashGuard.fired()));
|
|
360
|
+
let next = 0;
|
|
361
|
+
for (const [at, key] of keys.entries()) {
|
|
362
|
+
const known = memo.get(key);
|
|
363
|
+
if (known !== undefined && !fresh.includes(jobs[at]))
|
|
364
|
+
results.push(known);
|
|
365
|
+
else {
|
|
366
|
+
const output = ran[next++];
|
|
367
|
+
if (output !== undefined)
|
|
368
|
+
memo.set(key, output);
|
|
369
|
+
results.push(output);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const { entry, copies, members } = queue[index];
|
|
375
|
+
for (const copy of copies)
|
|
376
|
+
cover(copy.realm, copy.id, copy.sourceType);
|
|
377
|
+
const group = results[index];
|
|
378
|
+
if (group === undefined) {
|
|
379
|
+
stopped.push(...copies.map(({ url, realm, sourceType }) => ({ url, realm, sourceType })));
|
|
380
|
+
for (const member of members.slice(1)) {
|
|
381
|
+
cover(entry.realm, member.id, 'module');
|
|
382
|
+
const key = JSON.stringify([member.realm, member.url, member.sourceType]);
|
|
383
|
+
if (reachedBy.has(key))
|
|
384
|
+
continue;
|
|
385
|
+
reachedBy.add(key);
|
|
386
|
+
stopped.push({ url: member.url, realm: member.realm, sourceType: member.sourceType });
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
for (const member of members.slice(1))
|
|
391
|
+
reachedBy.add(JSON.stringify([member.realm, member.url, member.sourceType]));
|
|
392
|
+
const { url, sourceType, realm } = entry;
|
|
393
|
+
// A chunk a registry links is covered in the realm its own record is in too: a swept chunk a worker's group links
|
|
394
|
+
// is no window sweep. A module an import links keeps its own realm's analysis.
|
|
395
|
+
for (const member of group.members.slice(1)) {
|
|
396
|
+
cover(realm, member.url, member.sourceType);
|
|
397
|
+
const record = moduleIn(realm, member.url);
|
|
398
|
+
if (record !== undefined && registryOf(record).pushes.length > 0)
|
|
399
|
+
cover(record.realm, record.id, record.sourceType);
|
|
400
|
+
}
|
|
401
|
+
// Each member of the group other than its entry, by its artifact id, which the resolver names it by.
|
|
402
|
+
for (const member of group.members.slice(1)) {
|
|
403
|
+
const linked = group.moduleLinking[member.url];
|
|
404
|
+
const record = moduleIn(realm, member.url);
|
|
405
|
+
if (linked === undefined || record === undefined)
|
|
406
|
+
continue;
|
|
407
|
+
const key = JSON.stringify([record.id, record.realm, record.sourceType]);
|
|
408
|
+
const before = linking.get(key);
|
|
409
|
+
linking.set(key, before === undefined ? linked : { ...before, linkGroups: before.linkGroups + 1 });
|
|
410
|
+
}
|
|
411
|
+
for (const copy of copies) {
|
|
412
|
+
/** The artifact id an occurrence's artifact names: the copy for the entry, and each other member's own. */
|
|
413
|
+
const own = (artifact) => (artifact === url ? copy.id : artifact);
|
|
414
|
+
/** The URL of an artifact a trace step names. */
|
|
415
|
+
const urlOf = (artifact) => (artifact === url ? copy.url : (moduleIn(realm, artifact)?.url ?? artifact));
|
|
416
|
+
const owned = (write) => {
|
|
417
|
+
const lookup = write.lookup.artifact === undefined ? write.lookup : { ...write.lookup, artifact: own(write.lookup.artifact) };
|
|
418
|
+
const trace = write.trace.map((step) => step.kind !== 'module-link' && 'artifact' in step && typeof step.artifact === 'string'
|
|
419
|
+
? { ...step, artifact: urlOf(step.artifact) }
|
|
420
|
+
: step);
|
|
421
|
+
return { ...write, artifact: own(write.artifact), lookup, trace };
|
|
422
|
+
};
|
|
423
|
+
const linked = group.moduleLinking[url];
|
|
424
|
+
if (linked !== undefined)
|
|
425
|
+
linking.set(JSON.stringify([copy.id, realm, sourceType]), linked);
|
|
426
|
+
// A reference only the resolver can see is in the member whose code holds it (§19).
|
|
427
|
+
for (const { artifact: of, ...found } of group.references) {
|
|
428
|
+
const holder = of === url ? copy : moduleIn(realm, of);
|
|
429
|
+
if (holder === undefined || (found.kind === 'chunk') !== (found.load !== undefined))
|
|
430
|
+
continue;
|
|
431
|
+
const from = { url: holder.url, ...(holder.fetched === undefined ? {} : { fetched: holder.fetched }) };
|
|
432
|
+
const record = { realm, from, ...found };
|
|
433
|
+
const key = JSON.stringify(record);
|
|
434
|
+
if (!seen.has(key)) {
|
|
435
|
+
seen.add(key);
|
|
436
|
+
foundReferences.push(record);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
for (const occurrence of group.occurrences) {
|
|
440
|
+
const mine = owned(occurrence);
|
|
441
|
+
const key = JSON.stringify(mine);
|
|
442
|
+
if ((occurrenceAnalyses.get(key) ?? index) !== index)
|
|
443
|
+
continue;
|
|
444
|
+
occurrenceAnalyses.set(key, index);
|
|
445
|
+
found.push({ realm, occurrence: mine });
|
|
446
|
+
}
|
|
447
|
+
for (const declaration of group.lexicalDeclarations ?? []) {
|
|
448
|
+
const mine = declaration.artifact === url ? { ...declaration, artifact: copy.url } : declaration;
|
|
449
|
+
declarations.push({ realm, id: mine.artifact === copy.url ? copy.id : mine.artifact, declaration: mine });
|
|
450
|
+
}
|
|
451
|
+
// An ineffective write is copied as an occurrence is (§3 Record).
|
|
452
|
+
for (const write of group.ineffectiveWrites) {
|
|
453
|
+
const mine = owned(write);
|
|
454
|
+
const key = JSON.stringify(mine);
|
|
455
|
+
if ((ineffectiveAnalyses.get(key) ?? index) !== index)
|
|
456
|
+
continue;
|
|
457
|
+
ineffectiveAnalyses.set(key, index);
|
|
458
|
+
ineffectiveWrites.push(mine);
|
|
459
|
+
}
|
|
460
|
+
// A record's candidate targets, in the group's realm, are data for the surface filter (§12), and don't cross
|
|
461
|
+
// into the report.
|
|
462
|
+
for (const { kind, artifact: of, location, detail, candidates } of group.limitations) {
|
|
463
|
+
const artifact = { ...of, url: urlOf(of.url) };
|
|
464
|
+
const record = {
|
|
465
|
+
kind,
|
|
466
|
+
artifact,
|
|
467
|
+
...(location === undefined ? {} : { location }),
|
|
468
|
+
detail,
|
|
469
|
+
...(candidates === undefined ? {} : { candidates: candidates.map((name) => ({ realm, name })) }),
|
|
470
|
+
};
|
|
471
|
+
if (of.url !== url) {
|
|
472
|
+
const key = JSON.stringify(record);
|
|
473
|
+
if (memberRecords.has(key))
|
|
474
|
+
continue;
|
|
475
|
+
memberRecords.add(key);
|
|
476
|
+
}
|
|
477
|
+
limitations.push(record);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
statistics.push(group.stats);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
found,
|
|
484
|
+
declarations,
|
|
485
|
+
ineffectiveWrites,
|
|
486
|
+
limitations,
|
|
487
|
+
statistics,
|
|
488
|
+
linking,
|
|
489
|
+
fired,
|
|
490
|
+
stopped,
|
|
491
|
+
discovered,
|
|
492
|
+
references: foundReferences.sort(byReferenceOrder),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* The scan after it, until the report: noting lexical collisions, attribution, findings, completeness, comparison and
|
|
497
|
+
* the policy, over the last round's results.
|
|
498
|
+
*/
|
|
499
|
+
async function finish(config, deps, result) {
|
|
500
|
+
const { catalog, resolver } = deps;
|
|
501
|
+
const { found, declarations, ineffectiveWrites, limitations, statistics, linking, fired, stopped, discovered } = result;
|
|
502
|
+
const { artifacts } = discovered;
|
|
503
|
+
// A write that reads bare a name another classic script in its realm declares lexically is noted (§3).
|
|
504
|
+
const occurrences = lexicalCollisions(found, declarations, catalog);
|
|
505
|
+
// Each URL or inline copy of identical bytes is attributed through its own map (§2 Artifacts).
|
|
506
|
+
const read = artifacts.flatMap((artifact) => {
|
|
507
|
+
const { url, hash, origin, text, sourceMap, map } = artifact;
|
|
508
|
+
if (hash === null || text === null)
|
|
509
|
+
return [];
|
|
510
|
+
return [{ id: artifactId(artifact), url, hash, origin, text, map: map && { ...map, method: sourceMap.method } }];
|
|
511
|
+
});
|
|
512
|
+
const attributed = attribution(read, catalog, deps.lockfile ?? null);
|
|
513
|
+
// A scan whose crash guard fired reports no findings (§13): the groups that ran keep only their records and counts.
|
|
514
|
+
const detected = fired ? [] : buildFindings(attributed.attribute(occurrences), catalog);
|
|
515
|
+
const ineffective = fired ? [] : buildIneffectiveWrites(attributed.attributeIneffective(ineffectiveWrites), catalog);
|
|
516
|
+
// `--surface` limits what the scan reports, baselines and gates (§12): findings, ineffective writes and the
|
|
517
|
+
// `unresolved-holder` records.
|
|
518
|
+
const scoped = applySurface({ findings: detected, ineffectiveWrites: ineffective, limitations }, config.surface, catalog);
|
|
519
|
+
const { findings } = scoped;
|
|
520
|
+
const completeness = aggregate({
|
|
521
|
+
artifacts,
|
|
522
|
+
limitations: scoped.limitations,
|
|
523
|
+
html: discovered.html,
|
|
524
|
+
statistics,
|
|
525
|
+
budget: deps.budget,
|
|
526
|
+
crashGuard: { fired, stopped },
|
|
527
|
+
swept: discovered.swept,
|
|
528
|
+
findings,
|
|
529
|
+
sourceMapDirectories: discovered.sourceMapDirectories,
|
|
530
|
+
introducers: attributed.introducers,
|
|
531
|
+
});
|
|
532
|
+
// The strong versions of the findings' containing packages (§10 Version), which the report and baseline carry (§11).
|
|
533
|
+
const packages = packageVersions(findings);
|
|
534
|
+
const baseline = deps.baseline?.baseline ?? null;
|
|
535
|
+
// Each delta a catalog change since the baseline's catalog version could explain carries its change records, and
|
|
536
|
+
// each finding of a retained target says so (§4 Catalog versions).
|
|
537
|
+
const compared = withExplainedBy(withCatalogStatus(compare(findings, baseline, completeness, packages, deps.baseline?.previous ?? null), catalog), {
|
|
538
|
+
catalog,
|
|
539
|
+
changelog: deps.changelog,
|
|
540
|
+
baselineCatalogVersion: baseline === null ? null : baseline.catalogVersion,
|
|
541
|
+
surface: config.surface,
|
|
542
|
+
});
|
|
543
|
+
const failOn = config.command === 'scan' ? config.failOn : [];
|
|
544
|
+
const policed = applyPolicy(compared, { failOn, level: completeness.level, allowIncomplete: config.allowIncomplete });
|
|
545
|
+
return {
|
|
546
|
+
tool: {
|
|
547
|
+
monkeypatchlistVersion: deps.monkeypatchlistVersion,
|
|
548
|
+
catalogVersion: catalog.metadata.catalogVersion,
|
|
549
|
+
analyzerVersion: resolver.analyzerVersion,
|
|
550
|
+
fingerprintScheme,
|
|
551
|
+
},
|
|
552
|
+
application: { name: config.name },
|
|
553
|
+
config: {
|
|
554
|
+
scanConfigId: config.scanConfigId,
|
|
555
|
+
surface: config.surface,
|
|
556
|
+
mode: config.mode,
|
|
557
|
+
entries: config.entries,
|
|
558
|
+
mappings: config.mappings,
|
|
559
|
+
sourceMaps: config.sourceMaps,
|
|
560
|
+
lockfile: config.lockfile,
|
|
561
|
+
failOn,
|
|
562
|
+
},
|
|
563
|
+
gate: policed.gate,
|
|
564
|
+
outOfScopeBySurface: scoped.outOfScopeBySurface,
|
|
565
|
+
completeness,
|
|
566
|
+
baseline: deps.baseline,
|
|
567
|
+
packages,
|
|
568
|
+
findings: policed.findings,
|
|
569
|
+
ineffectiveWrites: scoped.ineffectiveWrites,
|
|
570
|
+
// An artifact no group analyzed links nothing (§6).
|
|
571
|
+
artifacts: artifacts.map((artifact) => ({
|
|
572
|
+
...artifact,
|
|
573
|
+
moduleLinking: linking.get(JSON.stringify([artifactId(artifact), artifact.realm, artifact.sourceType])) ?? {
|
|
574
|
+
idioms: [],
|
|
575
|
+
modules: 0,
|
|
576
|
+
unlinked: 0,
|
|
577
|
+
linkGroups: 0,
|
|
578
|
+
},
|
|
579
|
+
})),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The name an artifact's occurrences give it: its URL, and the URL it was fetched at when that isn't its URL. One key
|
|
584
|
+
* URL reached through two fetched URLs (§11) is an artifact record of each, both analyzed.
|
|
585
|
+
*/
|
|
586
|
+
function artifactId({ url, retrieval }) {
|
|
587
|
+
return idOf({ url, ...(retrieval.requestedUrl === null ? {} : { fetched: retrieval.requestedUrl }) });
|
|
588
|
+
}
|
|
589
|
+
/** The id of the artifact at `url` fetched at `fetched` (`artifactId`). */
|
|
590
|
+
function idOf({ url, fetched }) {
|
|
591
|
+
return fetched === undefined || fetched === url ? url : `${url} ${fetched}`;
|
|
592
|
+
}
|