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.
Files changed (221) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +61 -0
  3. package/README.md +183 -2
  4. package/catalog/catalog.json +552211 -0
  5. package/catalog/catalog.schema.json +787 -0
  6. package/catalog/changelog.json +3 -0
  7. package/dist/bin/monkeypatchlist.d.ts +2 -0
  8. package/dist/bin/monkeypatchlist.js +3 -0
  9. package/dist/catalog.d.ts +561 -0
  10. package/dist/catalog.js +58 -0
  11. package/dist/cli.d.ts +84 -0
  12. package/dist/cli.js +470 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +1 -0
  15. package/dist/lockfile-input.d.ts +9 -0
  16. package/dist/lockfile-input.js +91 -0
  17. package/dist/packages/analysis/binding.d.ts +2 -0
  18. package/dist/packages/analysis/binding.js +5 -0
  19. package/dist/packages/analysis/index.d.ts +10 -0
  20. package/dist/packages/analysis/index.js +13 -0
  21. package/dist/packages/analysis/job.d.ts +33 -0
  22. package/dist/packages/analysis/job.js +85 -0
  23. package/dist/packages/analysis/lib/absence-tags.d.ts +34 -0
  24. package/dist/packages/analysis/lib/absence-tags.js +340 -0
  25. package/dist/packages/analysis/lib/ast.d.ts +28 -0
  26. package/dist/packages/analysis/lib/ast.js +64 -0
  27. package/dist/packages/analysis/lib/binding.d.ts +36 -0
  28. package/dist/packages/analysis/lib/binding.js +66 -0
  29. package/dist/packages/analysis/lib/budget.d.ts +80 -0
  30. package/dist/packages/analysis/lib/budget.js +31 -0
  31. package/dist/packages/analysis/lib/catch-arms.d.ts +19 -0
  32. package/dist/packages/analysis/lib/catch-arms.js +351 -0
  33. package/dist/packages/analysis/lib/chunk-loads.d.ts +160 -0
  34. package/dist/packages/analysis/lib/chunk-loads.js +882 -0
  35. package/dist/packages/analysis/lib/chunk-registry.d.ts +45 -0
  36. package/dist/packages/analysis/lib/chunk-registry.js +334 -0
  37. package/dist/packages/analysis/lib/code-routes.d.ts +52 -0
  38. package/dist/packages/analysis/lib/code-routes.js +921 -0
  39. package/dist/packages/analysis/lib/collisions.d.ts +22 -0
  40. package/dist/packages/analysis/lib/collisions.js +103 -0
  41. package/dist/packages/analysis/lib/definitions.d.ts +76 -0
  42. package/dist/packages/analysis/lib/definitions.js +782 -0
  43. package/dist/packages/analysis/lib/early-exits.d.ts +39 -0
  44. package/dist/packages/analysis/lib/early-exits.js +161 -0
  45. package/dist/packages/analysis/lib/effect.d.ts +138 -0
  46. package/dist/packages/analysis/lib/effect.js +800 -0
  47. package/dist/packages/analysis/lib/global-object.d.ts +581 -0
  48. package/dist/packages/analysis/lib/global-object.js +8240 -0
  49. package/dist/packages/analysis/lib/keys.d.ts +33 -0
  50. package/dist/packages/analysis/lib/keys.js +82 -0
  51. package/dist/packages/analysis/lib/line-starts.d.ts +41 -0
  52. package/dist/packages/analysis/lib/line-starts.js +79 -0
  53. package/dist/packages/analysis/lib/link-group.d.ts +51 -0
  54. package/dist/packages/analysis/lib/link-group.js +487 -0
  55. package/dist/packages/analysis/lib/module-links.d.ts +117 -0
  56. package/dist/packages/analysis/lib/module-links.js +1060 -0
  57. package/dist/packages/analysis/lib/nested-code.d.ts +38 -0
  58. package/dist/packages/analysis/lib/nested-code.js +200 -0
  59. package/dist/packages/analysis/lib/numeric.d.ts +16 -0
  60. package/dist/packages/analysis/lib/numeric.js +216 -0
  61. package/dist/packages/analysis/lib/once-flags.d.ts +12 -0
  62. package/dist/packages/analysis/lib/once-flags.js +190 -0
  63. package/dist/packages/analysis/lib/parameter-arguments.d.ts +12 -0
  64. package/dist/packages/analysis/lib/parameter-arguments.js +34 -0
  65. package/dist/packages/analysis/lib/parse.d.ts +10 -0
  66. package/dist/packages/analysis/lib/parse.js +39 -0
  67. package/dist/packages/analysis/lib/partial.d.ts +60 -0
  68. package/dist/packages/analysis/lib/partial.js +518 -0
  69. package/dist/packages/analysis/lib/print.d.ts +3 -0
  70. package/dist/packages/analysis/lib/print.js +150 -0
  71. package/dist/packages/analysis/lib/reachability.d.ts +137 -0
  72. package/dist/packages/analysis/lib/reachability.js +1295 -0
  73. package/dist/packages/analysis/lib/resolver.d.ts +13 -0
  74. package/dist/packages/analysis/lib/resolver.js +2886 -0
  75. package/dist/packages/analysis/lib/restores.d.ts +42 -0
  76. package/dist/packages/analysis/lib/restores.js +482 -0
  77. package/dist/packages/analysis/lib/rewritten-deletes.d.ts +6 -0
  78. package/dist/packages/analysis/lib/rewritten-deletes.js +142 -0
  79. package/dist/packages/analysis/lib/scope.d.ts +110 -0
  80. package/dist/packages/analysis/lib/scope.js +443 -0
  81. package/dist/packages/analysis/lib/script-loads.d.ts +56 -0
  82. package/dist/packages/analysis/lib/script-loads.js +359 -0
  83. package/dist/packages/analysis/lib/summaries.d.ts +177 -0
  84. package/dist/packages/analysis/lib/summaries.js +498 -0
  85. package/dist/packages/analysis/lib/types.d.ts +632 -0
  86. package/dist/packages/analysis/lib/types.js +128 -0
  87. package/dist/packages/analysis/lib/wrap.d.ts +34 -0
  88. package/dist/packages/analysis/lib/wrap.js +407 -0
  89. package/dist/packages/analysis/lib/writes.d.ts +358 -0
  90. package/dist/packages/analysis/lib/writes.js +1612 -0
  91. package/dist/packages/analysis/visit-order.d.ts +3 -0
  92. package/dist/packages/analysis/visit-order.js +23 -0
  93. package/dist/packages/attribution/index.d.ts +233 -0
  94. package/dist/packages/attribution/index.js +444 -0
  95. package/dist/packages/attribution/lib/containing-package.d.ts +76 -0
  96. package/dist/packages/attribution/lib/containing-package.js +304 -0
  97. package/dist/packages/attribution/lib/first-party.d.ts +37 -0
  98. package/dist/packages/attribution/lib/first-party.js +153 -0
  99. package/dist/packages/attribution/lib/lockfile-graph.d.ts +99 -0
  100. package/dist/packages/attribution/lib/lockfile-graph.js +158 -0
  101. package/dist/packages/attribution/lib/lockfile.d.ts +33 -0
  102. package/dist/packages/attribution/lib/lockfile.js +330 -0
  103. package/dist/packages/attribution/lib/path-rules.d.ts +29 -0
  104. package/dist/packages/attribution/lib/path-rules.js +84 -0
  105. package/dist/packages/attribution/lib/pnpm.d.ts +4 -0
  106. package/dist/packages/attribution/lib/pnpm.js +133 -0
  107. package/dist/packages/attribution/lib/remote-url.d.ts +7 -0
  108. package/dist/packages/attribution/lib/remote-url.js +24 -0
  109. package/dist/packages/attribution/lib/source-map.d.ts +51 -0
  110. package/dist/packages/attribution/lib/source-map.js +125 -0
  111. package/dist/packages/attribution/lib/yaml.d.ts +9 -0
  112. package/dist/packages/attribution/lib/yaml.js +277 -0
  113. package/dist/packages/attribution/lib/yarn.d.ts +7 -0
  114. package/dist/packages/attribution/lib/yarn.js +227 -0
  115. package/dist/packages/baseline/index.d.ts +45 -0
  116. package/dist/packages/baseline/index.js +41 -0
  117. package/dist/packages/baseline/lib/error.d.ts +15 -0
  118. package/dist/packages/baseline/lib/error.js +25 -0
  119. package/dist/packages/baseline/lib/format.d.ts +49 -0
  120. package/dist/packages/baseline/lib/format.js +22 -0
  121. package/dist/packages/baseline/lib/migrations.d.ts +19 -0
  122. package/dist/packages/baseline/lib/migrations.js +7 -0
  123. package/dist/packages/baseline/lib/previous.d.ts +23 -0
  124. package/dist/packages/baseline/lib/previous.js +64 -0
  125. package/dist/packages/baseline/lib/read.d.ts +39 -0
  126. package/dist/packages/baseline/lib/read.js +219 -0
  127. package/dist/packages/baseline/lib/report.d.ts +25 -0
  128. package/dist/packages/baseline/lib/report.js +104 -0
  129. package/dist/packages/baseline/lib/skew.d.ts +51 -0
  130. package/dist/packages/baseline/lib/skew.js +75 -0
  131. package/dist/packages/compare/index.d.ts +120 -0
  132. package/dist/packages/compare/index.js +274 -0
  133. package/dist/packages/compare/lib/catalog-notes.d.ts +73 -0
  134. package/dist/packages/compare/lib/catalog-notes.js +224 -0
  135. package/dist/packages/completeness/index.d.ts +246 -0
  136. package/dist/packages/completeness/index.js +249 -0
  137. package/dist/packages/confidence/index.d.ts +54 -0
  138. package/dist/packages/confidence/index.js +71 -0
  139. package/dist/packages/config/index.d.ts +107 -0
  140. package/dist/packages/config/index.js +95 -0
  141. package/dist/packages/config/lib/command-line.d.ts +23 -0
  142. package/dist/packages/config/lib/command-line.js +239 -0
  143. package/dist/packages/config/lib/config-file.d.ts +11 -0
  144. package/dist/packages/config/lib/config-file.js +119 -0
  145. package/dist/packages/config/lib/scan-config-id.d.ts +28 -0
  146. package/dist/packages/config/lib/scan-config-id.js +54 -0
  147. package/dist/packages/config/lib/schema.d.ts +2 -0
  148. package/dist/packages/config/lib/schema.js +31 -0
  149. package/dist/packages/config/lib/types.d.ts +64 -0
  150. package/dist/packages/config/lib/types.js +6 -0
  151. package/dist/packages/config/lib/urls.d.ts +6 -0
  152. package/dist/packages/config/lib/urls.js +9 -0
  153. package/dist/packages/discovery/index.d.ts +277 -0
  154. package/dist/packages/discovery/index.js +1950 -0
  155. package/dist/packages/discovery/lib/import-map.d.ts +68 -0
  156. package/dist/packages/discovery/lib/import-map.js +180 -0
  157. package/dist/packages/discovery/lib/requests.d.ts +58 -0
  158. package/dist/packages/discovery/lib/requests.js +591 -0
  159. package/dist/packages/discovery/lib/scopes.d.ts +6 -0
  160. package/dist/packages/discovery/lib/scopes.js +286 -0
  161. package/dist/packages/discovery/lib/source-maps.d.ts +56 -0
  162. package/dist/packages/discovery/lib/source-maps.js +209 -0
  163. package/dist/packages/discovery/lib/swept-code.d.ts +27 -0
  164. package/dist/packages/discovery/lib/swept-code.js +107 -0
  165. package/dist/packages/discovery/lib/syntax.d.ts +17 -0
  166. package/dist/packages/discovery/lib/syntax.js +39 -0
  167. package/dist/packages/findings/index.d.ts +106 -0
  168. package/dist/packages/findings/index.js +254 -0
  169. package/dist/packages/findings/lib/scope.d.ts +38 -0
  170. package/dist/packages/findings/lib/scope.js +110 -0
  171. package/dist/packages/parallel/index.d.ts +2 -0
  172. package/dist/packages/parallel/index.js +5 -0
  173. package/dist/packages/parallel/lib/protocol.d.ts +15 -0
  174. package/dist/packages/parallel/lib/protocol.js +2 -0
  175. package/dist/packages/parallel/lib/runner.d.ts +36 -0
  176. package/dist/packages/parallel/lib/runner.js +96 -0
  177. package/dist/packages/parallel/lib/schedule.d.ts +16 -0
  178. package/dist/packages/parallel/lib/schedule.js +52 -0
  179. package/dist/packages/parallel/lib/worker.d.ts +1 -0
  180. package/dist/packages/parallel/lib/worker.js +18 -0
  181. package/dist/packages/policy/index.d.ts +36 -0
  182. package/dist/packages/policy/index.js +22 -0
  183. package/dist/packages/report/explain.d.ts +26 -0
  184. package/dist/packages/report/explain.js +99 -0
  185. package/dist/packages/report/index.d.ts +210 -0
  186. package/dist/packages/report/index.js +388 -0
  187. package/dist/packages/report/terminal.d.ts +11 -0
  188. package/dist/packages/report/terminal.js +331 -0
  189. package/dist/packages/source-store/fixture.d.ts +22 -0
  190. package/dist/packages/source-store/fixture.js +58 -0
  191. package/dist/packages/source-store/index.d.ts +278 -0
  192. package/dist/packages/source-store/index.js +322 -0
  193. package/dist/packages/source-store/lib/addresses.d.ts +6 -0
  194. package/dist/packages/source-store/lib/addresses.js +91 -0
  195. package/dist/packages/source-store/lib/connection.d.ts +15 -0
  196. package/dist/packages/source-store/lib/connection.js +135 -0
  197. package/dist/packages/source-store/lib/content-types.d.ts +16 -0
  198. package/dist/packages/source-store/lib/content-types.js +75 -0
  199. package/dist/packages/source-store/lib/data-urls.d.ts +10 -0
  200. package/dist/packages/source-store/lib/data-urls.js +85 -0
  201. package/dist/packages/source-store/lib/deadline.d.ts +18 -0
  202. package/dist/packages/source-store/lib/deadline.js +44 -0
  203. package/dist/packages/source-store/lib/exchange.d.ts +29 -0
  204. package/dist/packages/source-store/lib/exchange.js +112 -0
  205. package/dist/packages/source-store/lib/integrity.d.ts +19 -0
  206. package/dist/packages/source-store/lib/integrity.js +86 -0
  207. package/dist/packages/source-store/lib/outcomes.d.ts +12 -0
  208. package/dist/packages/source-store/lib/outcomes.js +22 -0
  209. package/dist/packages/source-store/lib/proxy.d.ts +11 -0
  210. package/dist/packages/source-store/lib/proxy.js +53 -0
  211. package/dist/packages/source-store/lib/session.d.ts +12 -0
  212. package/dist/packages/source-store/lib/session.js +139 -0
  213. package/dist/packages/source-store/lib/urls.d.ts +6 -0
  214. package/dist/packages/source-store/lib/urls.js +15 -0
  215. package/dist/packages/source-store/resolve.d.ts +59 -0
  216. package/dist/packages/source-store/resolve.js +45 -0
  217. package/dist/scan.d.ts +65 -0
  218. package/dist/scan.js +592 -0
  219. package/package.json +95 -4
  220. package/schema/config.schema.json +133 -0
  221. package/schema/report.schema.json +1905 -0
@@ -0,0 +1,219 @@
1
+ // Reading a baseline file's text (§11 Baseline, Format versions). The CLI reads the bytes; this checks them.
2
+ import { Ajv2020 } from 'ajv/dist/2020.js';
3
+ import { holders, realms, surfaces } from "../../../catalog.js";
4
+ import { effects } from "../../analysis/index.js";
5
+ import { provenances } from "../../attribution/index.js";
6
+ import { levels } from "../../completeness/index.js";
7
+ import { fingerprintScheme } from "../../findings/index.js";
8
+ import { BaselineError, problem } from "./error.js";
9
+ import { entry, formatVersion } from "./format.js";
10
+ import { migrations as shipped } from "./migrations.js";
11
+ import { previousOfReport } from "./previous.js";
12
+ import { baselineOfReport, isReport } from "./report.js";
13
+ import { semver } from "./skew.js";
14
+ export { BaselineError } from "./error.js";
15
+ const scopes = ['protected', 'all'];
16
+ /** The shape of a `formatVersion` 1 baseline. Keys it doesn't name are ignored. */
17
+ const schema = {
18
+ type: 'object',
19
+ required: [
20
+ 'formatVersion',
21
+ 'scanConfigId',
22
+ 'fingerprintScheme',
23
+ 'catalogVersion',
24
+ 'monkeypatchlistVersion',
25
+ 'completeness',
26
+ 'surface',
27
+ 'packages',
28
+ 'entries',
29
+ ],
30
+ properties: {
31
+ formatVersion: { const: formatVersion },
32
+ scanConfigId: { type: 'string' },
33
+ fingerprintScheme: { type: 'integer' },
34
+ catalogVersion: { type: 'integer', minimum: 1 },
35
+ monkeypatchlistVersion: { type: 'string', format: 'semver' },
36
+ completeness: { enum: levels },
37
+ surface: { enum: scopes },
38
+ packages: {
39
+ type: 'object',
40
+ additionalProperties: {
41
+ type: 'object',
42
+ required: ['version', 'versionSource'],
43
+ properties: { version: { type: 'string' }, versionSource: { type: 'string' } },
44
+ },
45
+ },
46
+ entries: {
47
+ type: 'array',
48
+ items: {
49
+ type: 'object',
50
+ required: ['fingerprint', 'target', 'surface', 'effect', 'locks', 'attribution'],
51
+ properties: {
52
+ fingerprint: { type: 'string', minLength: 1 },
53
+ target: {
54
+ type: 'object',
55
+ required: ['realm', 'holder', 'name'],
56
+ properties: {
57
+ realm: { enum: realms },
58
+ holder: { enum: holders },
59
+ name: { type: 'string', minLength: 1 },
60
+ shadows: { type: 'string', minLength: 1 },
61
+ },
62
+ },
63
+ surface: { enum: surfaces },
64
+ effect: { enum: effects },
65
+ locks: { enum: [true, false, 'unknown'] },
66
+ attribution: {
67
+ type: 'object',
68
+ required: ['key', 'provenance'],
69
+ properties: {
70
+ key: { type: 'string', minLength: 1 },
71
+ provenance: { enum: provenances },
72
+ package: { type: 'string', minLength: 1 },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ },
79
+ };
80
+ let validate;
81
+ /**
82
+ * The baseline in `text`, or a `BaselineError` that says why it isn't one. The baseline comes back with its keys in the
83
+ * fixed order `baseline init` writes, without keys this monkeypatchlist doesn't read.
84
+ *
85
+ * - A `formatVersion` newer than this monkeypatchlist's is an error, and an older one is migrated in memory (§11 Format
86
+ * versions), by `migrations`. An older one that no migration reads is not a readable baseline.
87
+ * - A fingerprint scheme older than this monkeypatchlist's is migrated in memory, by `migrations`. A scheme that can't be
88
+ * migrated (a newer one, or an older one that no migration reads) is not a readable baseline: a hard error under
89
+ * `--fail-on`, and a scan without a baseline otherwise (§4 Version skew, §11 No baseline).
90
+ * - A fingerprint that two entries share is an error.
91
+ * - With `options.report`, the text may be a full scan report instead, read as the baseline `baseline update` would
92
+ * have written from its scan (§11 Baseline, ADR 0020). Without it, a report is not a readable baseline.
93
+ */
94
+ export function readBaseline(text, migrations = shipped, options = {}) {
95
+ let value;
96
+ try {
97
+ value = parseJson(text);
98
+ }
99
+ catch (error) {
100
+ throw new BaselineError(`it is not valid JSON: ${error.message}`);
101
+ }
102
+ // `scan --baseline` also reads a full scan report, as the baseline `baseline update` would write from its scan (§11
103
+ // Baseline, ADR 0020). Nothing else reads one as a baseline.
104
+ let previous = null;
105
+ if (isReport(value)) {
106
+ if (options.report === undefined)
107
+ throw new BaselineError('it is a scan report, not a baseline file');
108
+ const report = value;
109
+ value = baselineOfReport(report, options.report);
110
+ previous = previousOfReport(report, options.report.reportVersion).values;
111
+ }
112
+ const version = value?.formatVersion;
113
+ if (typeof version === 'number' && version > formatVersion) {
114
+ throw new BaselineError(`its formatVersion ${version} is newer than this monkeypatchlist reads (${formatVersion})`, {
115
+ newerFormat: true,
116
+ });
117
+ }
118
+ if (typeof version === 'number' && Number.isInteger(version)) {
119
+ for (let from = version; from < formatVersion; from++) {
120
+ const migrate = migrations.format.get(from);
121
+ if (migrate === undefined) {
122
+ throw new BaselineError(`its formatVersion ${version} is older than this monkeypatchlist can migrate to the one it reads (${formatVersion})`);
123
+ }
124
+ // A format migration reads the file before any schema check, so one that throws on it read a file that isn't a
125
+ // baseline in that format.
126
+ try {
127
+ value = migrate(value);
128
+ }
129
+ catch (error) {
130
+ throw new BaselineError(`it is not a baseline in formatVersion ${from}: ${error.message}`);
131
+ }
132
+ }
133
+ }
134
+ // `monkeypatchlistVersion` is checked as a named format, so a bad one is reported as not `semver`, not by the regex.
135
+ validate ??= new Ajv2020({ allErrors: true, strict: true, formats: { semver } }).compile(schema);
136
+ if (!validate(value)) {
137
+ const problems = (validate.errors ?? []).map(problem);
138
+ throw new BaselineError(`it is not a baseline:\n${problems.map((line) => ` ${line}`).join('\n')}`);
139
+ }
140
+ const baseline = value;
141
+ const scheme = baseline.fingerprintScheme;
142
+ let migrated = baseline.entries;
143
+ for (let from = scheme; from !== fingerprintScheme; from++) {
144
+ const migrate = from < fingerprintScheme ? migrations.fingerprintScheme.get(from) : undefined;
145
+ if (migrate === undefined) {
146
+ // A newer scheme comes from a newer monkeypatchlist: replacing it with this one is a downgrade (§4 Version skew).
147
+ const [skew, remedy] = scheme > fingerprintScheme
148
+ ? [
149
+ 'is newer than',
150
+ 'Upgrade monkeypatchlist, or write a new baseline with monkeypatchlist baseline init --allow-downgrade',
151
+ ]
152
+ : ["can't be migrated to", 'Write a new baseline with monkeypatchlist baseline init'];
153
+ throw new BaselineError(`its fingerprint scheme ${scheme} ${skew} this monkeypatchlist's (${fingerprintScheme}), so its fingerprints never ` +
154
+ `match this scan's. ${remedy}`);
155
+ }
156
+ migrated = migrate(migrated);
157
+ }
158
+ const entries = migrated.map(entry);
159
+ const seen = new Set();
160
+ for (const { fingerprint } of entries) {
161
+ if (seen.has(fingerprint))
162
+ throw new BaselineError(`it lists the fingerprint ${fingerprint} twice`);
163
+ seen.add(fingerprint);
164
+ }
165
+ const { catalogVersion, monkeypatchlistVersion } = baseline;
166
+ return {
167
+ baseline: {
168
+ formatVersion,
169
+ scanConfigId: baseline.scanConfigId,
170
+ fingerprintScheme,
171
+ catalogVersion,
172
+ monkeypatchlistVersion,
173
+ completeness: baseline.completeness,
174
+ surface: baseline.surface,
175
+ packages: Object.fromEntries(Object.entries(baseline.packages).map(([name, { version, versionSource }]) => [name, { version, versionSource }])),
176
+ entries,
177
+ },
178
+ written: { formatVersion: version, fingerprintScheme: scheme, catalogVersion, monkeypatchlistVersion },
179
+ previous,
180
+ };
181
+ }
182
+ /**
183
+ * The versions a baseline's text was written with (§4 Version skew), for `baseline init` to tell whether the file it
184
+ * replaces came from a newer version. They are the versions `readBaseline` reads. A text that isn't a baseline this
185
+ * monkeypatchlist reads (a newer format or fingerprint scheme, or a newer catalog's holder) gives each version its header
186
+ * records in a well-formed value, and a text that isn't a JSON object gives none.
187
+ */
188
+ export function writtenVersions(text, migrations = shipped) {
189
+ try {
190
+ return readBaseline(text, migrations).written;
191
+ }
192
+ catch (error) {
193
+ if (!(error instanceof BaselineError))
194
+ throw error;
195
+ }
196
+ let value;
197
+ try {
198
+ value = parseJson(text);
199
+ }
200
+ catch {
201
+ return {};
202
+ }
203
+ if (typeof value !== 'object' || value === null)
204
+ return {};
205
+ const header = value;
206
+ const written = {};
207
+ for (const name of ['formatVersion', 'fingerprintScheme', 'catalogVersion']) {
208
+ if (Number.isInteger(header[name]))
209
+ written[name] = header[name];
210
+ }
211
+ const { monkeypatchlistVersion } = header;
212
+ if (typeof monkeypatchlistVersion === 'string' && semver.test(monkeypatchlistVersion))
213
+ written.monkeypatchlistVersion = monkeypatchlistVersion;
214
+ return written;
215
+ }
216
+ /** The JSON value in a baseline file's `text`, with a leading UTF-8 byte order mark dropped. */
217
+ function parseJson(text) {
218
+ return JSON.parse(text.startsWith('') ? text.slice(1) : text);
219
+ }
@@ -0,0 +1,25 @@
1
+ /** How `scan --baseline` reads a report (§11 Baseline). */
2
+ export interface ReportReading {
3
+ /** The `reportVersion` this monkeypatchlist writes: a newer one is a hard error, as a newer `formatVersion` is (§11). */
4
+ reportVersion: number;
5
+ /**
6
+ * `--allow-incomplete`: a report whose completeness is `incomplete` is a readable baseline only with it, and then
7
+ * reads as `baseline update --allow-incomplete` would write it.
8
+ */
9
+ allowIncomplete: boolean;
10
+ }
11
+ /** Whether a parsed JSON value is a scan report rather than a baseline file: only a report has a `reportVersion`. */
12
+ export declare function isReport(value: unknown): boolean;
13
+ /**
14
+ * The baseline value `baseline update` would have written from the scan `value` reports (§11 Baseline, ADR 0020), or
15
+ * a `BaselineError` that says why the report is no readable baseline:
16
+ *
17
+ * - Every finding except a `removed` one is an entry, `added` and `changed` included, under its current fingerprint,
18
+ * and `unconfirmed` entries carry forward. The header is the scan's: its scan-config id, surface, completeness level,
19
+ * versions and `packages` map.
20
+ * - An `incomplete` report is refused without `--allow-incomplete`.
21
+ * - A report whose crash guard fired holds no findings, so `update --allow-incomplete` would have left its baseline as
22
+ * it was (§13): it gives back that baseline, its header and its `unconfirmed` entries. One with no baseline of its own
23
+ * is refused even with the flag, as `baseline init` refuses its scan.
24
+ */
25
+ export declare function baselineOfReport(value: unknown, reading: ReportReading): unknown;
@@ -0,0 +1,104 @@
1
+ // A full scan JSON report read as a baseline (§11 Baseline, ADR 0020): as if `baseline update` had just been run on
2
+ // that scan. This turns the report's value into the baseline value `update` would have written, which `readBaseline`
3
+ // then checks like a file's.
4
+ import { Ajv2020 } from 'ajv/dist/2020.js';
5
+ import { BaselineError, problem } from "./error.js";
6
+ import { formatVersion } from "./format.js";
7
+ /**
8
+ * The parts of a report this reads. The baseline value made from them is checked against the baseline schema, so this
9
+ * checks only what it reads to make one.
10
+ */
11
+ const schema = {
12
+ type: 'object',
13
+ required: ['reportVersion', 'tool', 'config', 'completeness', 'baseline', 'packages', 'findings'],
14
+ properties: {
15
+ reportVersion: { type: 'integer' },
16
+ tool: {
17
+ type: 'object',
18
+ required: ['monkeypatchlistVersion', 'catalogVersion', 'fingerprintScheme'],
19
+ properties: { monkeypatchlistVersion: {}, catalogVersion: {}, fingerprintScheme: {} },
20
+ },
21
+ config: { type: 'object', required: ['scanConfigId', 'surface'], properties: { scanConfigId: {}, surface: {} } },
22
+ completeness: {
23
+ type: 'object',
24
+ required: ['level', 'counters'],
25
+ properties: {
26
+ level: {},
27
+ counters: {
28
+ type: 'object',
29
+ required: ['statistics'],
30
+ properties: { statistics: { type: 'object', required: ['crashGuard'], properties: { crashGuard: {} } } },
31
+ },
32
+ },
33
+ },
34
+ baseline: {
35
+ oneOf: [{ type: 'null' }, { type: 'object', required: ['header'], properties: { header: { type: 'object' } } }],
36
+ },
37
+ packages: { type: 'object' },
38
+ findings: { type: 'array', items: { type: 'object' } },
39
+ },
40
+ };
41
+ let validate;
42
+ /** Whether a parsed JSON value is a scan report rather than a baseline file: only a report has a `reportVersion`. */
43
+ export function isReport(value) {
44
+ return typeof value === 'object' && value !== null && 'reportVersion' in value;
45
+ }
46
+ /**
47
+ * The baseline value `baseline update` would have written from the scan `value` reports (§11 Baseline, ADR 0020), or
48
+ * a `BaselineError` that says why the report is no readable baseline:
49
+ *
50
+ * - Every finding except a `removed` one is an entry, `added` and `changed` included, under its current fingerprint,
51
+ * and `unconfirmed` entries carry forward. The header is the scan's: its scan-config id, surface, completeness level,
52
+ * versions and `packages` map.
53
+ * - An `incomplete` report is refused without `--allow-incomplete`.
54
+ * - A report whose crash guard fired holds no findings, so `update --allow-incomplete` would have left its baseline as
55
+ * it was (§13): it gives back that baseline, its header and its `unconfirmed` entries. One with no baseline of its own
56
+ * is refused even with the flag, as `baseline init` refuses its scan.
57
+ */
58
+ export function baselineOfReport(value, reading) {
59
+ validate ??= new Ajv2020({ allErrors: true, strict: true }).compile(schema);
60
+ if (!validate(value)) {
61
+ const problems = (validate.errors ?? []).map(problem);
62
+ throw new BaselineError(`it is not a scan report:\n${problems.map((line) => ` ${line}`).join('\n')}`);
63
+ }
64
+ const report = value;
65
+ if (report.reportVersion > reading.reportVersion) {
66
+ throw new BaselineError(`it is a scan report in reportVersion ${report.reportVersion}, newer than this monkeypatchlist reads (${reading.reportVersion})`, { newerFormat: true });
67
+ }
68
+ if (report.reportVersion < reading.reportVersion) {
69
+ throw new BaselineError(`it is a scan report in reportVersion ${report.reportVersion}, older than this monkeypatchlist reads (${reading.reportVersion})`);
70
+ }
71
+ const { level } = report.completeness;
72
+ const fired = report.completeness.counters.statistics.crashGuard === 'fired';
73
+ if (fired && report.baseline === null) {
74
+ throw new BaselineError("it is the report of a scan whose crash guard fired, with no baseline of its own: it holds no findings, so it isn't " +
75
+ 'read as a baseline even under --allow-incomplete, as baseline init refuses such a scan');
76
+ }
77
+ if (level === 'incomplete' && !reading.allowIncomplete) {
78
+ throw new BaselineError("it is the report of an incomplete scan (its completeness says what was not analyzed): pass --allow-incomplete to " +
79
+ 'read it as baseline update --allow-incomplete would write it');
80
+ }
81
+ const entries = report.findings
82
+ .filter((finding) => finding.baselineState !== 'removed')
83
+ .map(({ fingerprint, target, surface, effect, locks, attribution }) => ({
84
+ fingerprint,
85
+ target,
86
+ surface,
87
+ effect,
88
+ locks,
89
+ attribution,
90
+ }));
91
+ if (fired)
92
+ return { ...report.baseline.header, formatVersion, entries };
93
+ return {
94
+ formatVersion,
95
+ scanConfigId: report.config.scanConfigId,
96
+ fingerprintScheme: report.tool.fingerprintScheme,
97
+ catalogVersion: report.tool.catalogVersion,
98
+ monkeypatchlistVersion: report.tool.monkeypatchlistVersion,
99
+ completeness: level,
100
+ surface: report.config.surface,
101
+ packages: report.packages,
102
+ entries,
103
+ };
104
+ }
@@ -0,0 +1,51 @@
1
+ /** The versions a baseline's header records (§11 Baseline), or that a scan runs with (§15 `tool`). */
2
+ export interface Versions {
3
+ formatVersion: number;
4
+ fingerprintScheme: number;
5
+ catalogVersion: number;
6
+ monkeypatchlistVersion: string;
7
+ }
8
+ /** A version on each side: the baseline's, as its file was written, and the scan's. */
9
+ export interface VersionPair<Version> {
10
+ baseline: Version;
11
+ scan: Version;
12
+ }
13
+ /**
14
+ * The scan-level note that the baseline was written by another version (§4 Version skew, §15 `baseline.notes`). It
15
+ * names each version on both sides.
16
+ */
17
+ export interface VersionSkew {
18
+ kind: 'version-skew';
19
+ /**
20
+ * The side written by the newer version. `baseline` when any of the baseline's versions is newer than the scan's: a
21
+ * stale install, which scans with a warning and no `explainedBy`, and which `baseline update` refuses to rewrite
22
+ * without `--allow-downgrade`. Otherwise `scan`: an upgrade, where a delta without `explainedBy` may come from analyzer
23
+ * changes.
24
+ */
25
+ newer: 'baseline' | 'scan';
26
+ formatVersion: VersionPair<number>;
27
+ fingerprintScheme: VersionPair<number>;
28
+ catalogVersion: VersionPair<number>;
29
+ monkeypatchlistVersion: VersionPair<string>;
30
+ }
31
+ /** A scan-level note on the baseline (§15 `baseline.notes`). */
32
+ export type BaselineNote = VersionSkew;
33
+ /**
34
+ * The version-skew note for a baseline written with the versions `baseline` and scanned with `scan`, or null when they
35
+ * are all the same.
36
+ */
37
+ export declare function versionSkew(baseline: Versions, scan: Versions): VersionSkew | null;
38
+ /** A version a baseline was written with that is newer than this monkeypatchlist's, with the version on each side. */
39
+ export type NewerVersion = {
40
+ [Name in keyof Versions]: {
41
+ name: Name;
42
+ } & VersionPair<Versions[Name]>;
43
+ }[keyof Versions];
44
+ /**
45
+ * The versions of `baseline`'s that are newer than this monkeypatchlist's (`scan`), in the order of the baseline header (§4
46
+ * Version skew). A baseline with any was written by a newer monkeypatchlist or catalog: `baseline init` and `baseline update`
47
+ * refuse to replace it unless `--allow-downgrade` is given. A version `baseline` lacks is not compared.
48
+ */
49
+ export declare function newerVersions(baseline: Partial<Versions>, scan: Versions): NewerVersion[];
50
+ /** A semantic version: `major.minor.patch`, an optional `-prerelease` and optional `+build` metadata. */
51
+ export declare const semver: RegExp;
@@ -0,0 +1,75 @@
1
+ // Version skew (§4 Version skew, ADR 0008): a baseline written by another monkeypatchlist version than the one scanning.
2
+ /**
3
+ * The version-skew note for a baseline written with the versions `baseline` and scanned with `scan`, or null when they
4
+ * are all the same.
5
+ */
6
+ export function versionSkew(baseline, scan) {
7
+ const order = versionNames.map((name) => compare(name, baseline, scan));
8
+ if (order.every((sign) => sign === 0))
9
+ return null;
10
+ return {
11
+ kind: 'version-skew',
12
+ newer: order.some((sign) => sign > 0) ? 'baseline' : 'scan',
13
+ formatVersion: { baseline: baseline.formatVersion, scan: scan.formatVersion },
14
+ fingerprintScheme: { baseline: baseline.fingerprintScheme, scan: scan.fingerprintScheme },
15
+ catalogVersion: { baseline: baseline.catalogVersion, scan: scan.catalogVersion },
16
+ monkeypatchlistVersion: { baseline: baseline.monkeypatchlistVersion, scan: scan.monkeypatchlistVersion },
17
+ };
18
+ }
19
+ /**
20
+ * The versions of `baseline`'s that are newer than this monkeypatchlist's (`scan`), in the order of the baseline header (§4
21
+ * Version skew). A baseline with any was written by a newer monkeypatchlist or catalog: `baseline init` and `baseline update`
22
+ * refuse to replace it unless `--allow-downgrade` is given. A version `baseline` lacks is not compared.
23
+ */
24
+ export function newerVersions(baseline, scan) {
25
+ return versionNames.flatMap((name) => {
26
+ if (baseline[name] === undefined || compare(name, baseline, scan) <= 0)
27
+ return [];
28
+ return [{ name, baseline: baseline[name], scan: scan[name] }];
29
+ });
30
+ }
31
+ /** The versions, in the order of the baseline header (§11 Baseline). */
32
+ const versionNames = ['formatVersion', 'fingerprintScheme', 'catalogVersion', 'monkeypatchlistVersion'];
33
+ /** Orders one version of `a` against the same version of `b`: negative when `a`'s is older, positive when newer. */
34
+ function compare(name, a, b) {
35
+ return name === 'monkeypatchlistVersion'
36
+ ? compareVersions(a.monkeypatchlistVersion, b.monkeypatchlistVersion)
37
+ : Math.sign(a[name] - b[name]);
38
+ }
39
+ /** A semantic version: `major.minor.patch`, an optional `-prerelease` and optional `+build` metadata. */
40
+ export const semver = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
41
+ /**
42
+ * Orders two monkeypatchlist versions by semantic-version precedence: negative when `a` is older, positive when it is newer,
43
+ * 0 when they rank the same. Build metadata is ignored, and a prerelease ranks below its release.
44
+ */
45
+ function compareVersions(a, b) {
46
+ const [x, y] = [parse(a), parse(b)];
47
+ for (let i = 0; i < 3; i++)
48
+ if (x.core[i] !== y.core[i])
49
+ return Math.sign(x.core[i] - y.core[i]);
50
+ // A release ranks above its prereleases.
51
+ if (x.prerelease.length === 0 || y.prerelease.length === 0) {
52
+ return Math.sign(y.prerelease.length) - Math.sign(x.prerelease.length);
53
+ }
54
+ for (let i = 0; i < Math.min(x.prerelease.length, y.prerelease.length); i++) {
55
+ const order = compareIdentifiers(x.prerelease[i], y.prerelease[i]);
56
+ if (order !== 0)
57
+ return order;
58
+ }
59
+ return Math.sign(x.prerelease.length - y.prerelease.length);
60
+ }
61
+ function parse(version) {
62
+ const match = semver.exec(version);
63
+ if (match === null)
64
+ throw new Error(`'${version}' is not a semantic version`);
65
+ return { core: match.slice(1, 4).map(Number), prerelease: match[4]?.split('.') ?? [] };
66
+ }
67
+ /** Numeric identifiers rank by value and below alphanumeric ones, which rank by ASCII order. */
68
+ function compareIdentifiers(a, b) {
69
+ const numeric = (identifier) => /^\d+$/.test(identifier);
70
+ if (numeric(a) && numeric(b))
71
+ return Math.sign(Number(a) - Number(b));
72
+ if (numeric(a) !== numeric(b))
73
+ return numeric(a) ? -1 : 1;
74
+ return a < b ? -1 : a > b ? 1 : 0;
75
+ }
@@ -0,0 +1,120 @@
1
+ import { type Effect, type InstallContext, type Reachability, type ReachabilityBasis } from '../analysis/index.ts';
2
+ import type { Baseline, BaselineState, PackageVersion, PreviousValues } from '../baseline/index.ts';
3
+ import type { Completeness } from '../completeness/index.ts';
4
+ import type { Confidence } from '../confidence/index.ts';
5
+ import { type Finding } from '../findings/index.ts';
6
+ import type { ExplainingRecord } from './lib/catalog-notes.ts';
7
+ export { withCatalogStatus, withExplainedBy, type ExplainingRecord } from './lib/catalog-notes.ts';
8
+ /** How a finding compares with the baseline (§11). The baseline package defines it, since what it accepts depends on it. */
9
+ export type { BaselineState } from '../baseline/index.ts';
10
+ /** The matching pass that paired a finding with a baseline entry (§11 Baseline states and matching). */
11
+ export type MatchedBy = 'fingerprint' | 'effect' | 'target-key';
12
+ /**
13
+ * Why a paired finding is `changed`, with the baseline entry's value before and the finding's after (§11 `changed`). An
14
+ * `attribution` reason's values are attribution keys.
15
+ */
16
+ export type ChangedReason = {
17
+ reason: 'attribution';
18
+ before: string;
19
+ after: string;
20
+ } | {
21
+ reason: 'effect';
22
+ before: Effect;
23
+ after: Effect;
24
+ } | {
25
+ reason: 'locks';
26
+ before: boolean;
27
+ after: boolean;
28
+ };
29
+ /**
30
+ * A note on a finding (§11, §15 Findings). `rekeyed`: the target-key pass paired it across `unknown`, from the paired
31
+ * baseline entries' attribution keys (`before`) to its own (`after`). `shared-target-key`: it stays `added` or `removed`
32
+ * because its target-key group is ambiguous, and `fingerprints` are the other side's known keys it could have paired
33
+ * with. `previous-version`: its containing package has a strong version on both sides and they differ (§11
34
+ * `previousVersion`), from the baseline's version (`before`) to the scan's (`after`). `reachability-change` and
35
+ * `install-context-change`: its reachability or install context differs from the previous scan's value for the finding
36
+ * it paired with (§11 Never `changed`), from that value (`before`) to its own (`after`). They never decide a state.
37
+ */
38
+ export type FindingNote = {
39
+ kind: 'rekeyed';
40
+ before: string[];
41
+ after: string;
42
+ } | {
43
+ kind: 'shared-target-key';
44
+ fingerprints: string[];
45
+ } | {
46
+ kind: 'previous-version';
47
+ package: string;
48
+ before: string;
49
+ after: string;
50
+ } | {
51
+ kind: 'reachability-change';
52
+ before: Reachability;
53
+ after: Reachability;
54
+ } | {
55
+ kind: 'install-context-change';
56
+ before: InstallContext;
57
+ after: InstallContext;
58
+ };
59
+ /**
60
+ * A scanned finding with its baseline state, or a baseline entry the scan didn't find. That one has the entry's readable
61
+ * context, no occurrences, and no confidence, which only occurrences give (§15 Findings).
62
+ */
63
+ export interface ComparedFinding extends Omit<Finding, 'confidence' | 'reachability' | 'reachabilityBasis' | 'installContext'> {
64
+ confidence: Confidence | null;
65
+ /** Null for a baseline entry the scan didn't find, which has no occurrences (§15 Findings). */
66
+ reachability: Reachability | null;
67
+ /** Its basis (§7), null as `reachability` is. */
68
+ reachabilityBasis: ReachabilityBasis | null;
69
+ /** Null for a baseline entry the scan didn't find, as `reachability` is (§15 Findings). */
70
+ installContext: InstallContext | null;
71
+ /** Null when there is no readable baseline (§11 No baseline). */
72
+ baselineState: BaselineState | null;
73
+ /** The pass that paired it with a baseline entry, or null when nothing did. */
74
+ matchedBy: MatchedBy | null;
75
+ /** Before → after for each reason the finding is `changed`, in the §11 table's order. Empty unless it is `changed`. */
76
+ changedReasons: ChangedReason[];
77
+ /**
78
+ * Notes on how it was matched (§11), `rekeyed` or `shared-target-key`, then its `previous-version`, then its
79
+ * `reachability-change` and `install-context-change`.
80
+ */
81
+ notes: FindingNote[];
82
+ /** Set when its target, or the member it shadows, is a retained target (§4 Catalog versions): information only. */
83
+ catalogStatus?: 'retained';
84
+ /**
85
+ * The catalog changelog's records that could explain this delta (§4 Catalog versions), in changelog order. Absent when
86
+ * none does, and never set on `unconfirmed` or on `existing` without a `rekeyed` note. Never baselined.
87
+ */
88
+ explainedBy?: ExplainingRecord[];
89
+ }
90
+ /**
91
+ * Compares findings with the baseline (§11 Baseline states and matching), in order:
92
+ *
93
+ * 1. By exact fingerprint.
94
+ * 2. The effect pass: a finding and an entry left over with the same realm, target and attribution key but a different
95
+ * effect pair when each is the only one left in that group (1:1). A paired finding is `changed`, reason `effect`.
96
+ * 3. The target-key pass, over the findings and entries left over by target key `(realm, target, effect)`, plus any
97
+ * `unknown` finding or entry the exact pass matched. A known key pairs with a known key 1:1 only: `changed`,
98
+ * reason `attribution`. An `unknown` finding pairs with every known entry left over in its group, and an `unknown`
99
+ * entry with every known finding left over, as a `rekeyed` pair; both rules apply in one group. The known
100
+ * findings and entries of a group with both but not 1:1 that nothing pairs stay `added` and `removed`, with a
101
+ * `shared-target-key` note.
102
+ *
103
+ * A paired finding whose `locks` went `true` ↔ `false` against any entry it pairs with is `changed`, reason `locks`,
104
+ * once per such entry; `unknown` on either side is no change. A paired finding with no reason is `existing`, and a
105
+ * finding nothing paired is `added`. An entry that matches no finding is `unconfirmed` when the scan could not have seen
106
+ * it (§13 Effects on the baseline and the gate): it is `incomplete`, a link group exhausted its step budget, or the entry
107
+ * is remote and a remote script was skipped. It is `removed` otherwise. Without a baseline, every finding's state is
108
+ * null. The result is sorted by fingerprint.
109
+ *
110
+ * Whatever its state, a finding whose containing package has a strong version in both the scan's `packages` map and
111
+ * the baseline's, and they differ, gets a `previous-version` note (§11 `previousVersion`). The version never decides a
112
+ * state.
113
+ *
114
+ * With `previous`, the values of a previous scan, an `existing` or `changed` finding whose reachability or install
115
+ * context differs from the previous value of a baseline entry it paired with gets a `reachability-change` or
116
+ * `install-context-change` note, one per distinct previous value (§11 Never `changed`). The previous value is the
117
+ * one under the entry's fingerprint, so it follows the pairing, a `rekeyed` one included. These notes never decide a
118
+ * state, and need a baseline.
119
+ */
120
+ export declare function compare(findings: readonly Finding[], baseline: Baseline | null, completeness: Pick<Completeness, 'level' | 'limitations' | 'counters'>, packages?: Readonly<Record<string, PackageVersion>>, previous?: PreviousValues | null): ComparedFinding[];