@keithadler/frostjs 0.1.0 → 0.4.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 (72) hide show
  1. package/README.md +131 -8
  2. package/dist/audit.d.ts +71 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +202 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/capabilities.d.ts +22 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +140 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/cli/args.d.ts +5 -3
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +29 -2
  13. package/dist/cli/args.js.map +1 -1
  14. package/dist/cli/run.d.ts.map +1 -1
  15. package/dist/cli/run.js +113 -5
  16. package/dist/cli/run.js.map +1 -1
  17. package/dist/extract/html.d.ts +11 -0
  18. package/dist/extract/html.d.ts.map +1 -1
  19. package/dist/extract/html.js +58 -0
  20. package/dist/extract/html.js.map +1 -1
  21. package/dist/extract/index.d.ts +2 -0
  22. package/dist/extract/index.d.ts.map +1 -1
  23. package/dist/extract/index.js +19 -0
  24. package/dist/extract/index.js.map +1 -1
  25. package/dist/extract/recognizers/device.d.ts +10 -0
  26. package/dist/extract/recognizers/device.d.ts.map +1 -0
  27. package/dist/extract/recognizers/device.js +46 -0
  28. package/dist/extract/recognizers/device.js.map +1 -0
  29. package/dist/extract/recognizers/dom-escape.d.ts.map +1 -1
  30. package/dist/extract/recognizers/dom-escape.js +11 -0
  31. package/dist/extract/recognizers/dom-escape.js.map +1 -1
  32. package/dist/extract/recognizers/message.d.ts +16 -0
  33. package/dist/extract/recognizers/message.d.ts.map +1 -0
  34. package/dist/extract/recognizers/message.js +92 -0
  35. package/dist/extract/recognizers/message.js.map +1 -0
  36. package/dist/extract/recognizers/network.d.ts +10 -2
  37. package/dist/extract/recognizers/network.d.ts.map +1 -1
  38. package/dist/extract/recognizers/network.js +49 -6
  39. package/dist/extract/recognizers/network.js.map +1 -1
  40. package/dist/extract/taint.d.ts +36 -0
  41. package/dist/extract/taint.d.ts.map +1 -0
  42. package/dist/extract/taint.js +503 -0
  43. package/dist/extract/taint.js.map +1 -0
  44. package/dist/extract/target.d.ts.map +1 -1
  45. package/dist/extract/target.js +23 -1
  46. package/dist/extract/target.js.map +1 -1
  47. package/dist/policy/compile.d.ts +3 -1
  48. package/dist/policy/compile.d.ts.map +1 -1
  49. package/dist/policy/compile.js +1 -0
  50. package/dist/policy/compile.js.map +1 -1
  51. package/dist/policy/config.d.ts.map +1 -1
  52. package/dist/policy/config.js +48 -1
  53. package/dist/policy/config.js.map +1 -1
  54. package/dist/policy/index.js +1 -1
  55. package/dist/policy/index.js.map +1 -1
  56. package/dist/policy/parse.d.ts +7 -0
  57. package/dist/policy/parse.d.ts.map +1 -1
  58. package/dist/policy/parse.js +24 -1
  59. package/dist/policy/parse.js.map +1 -1
  60. package/dist/policy/vocabulary.d.ts.map +1 -1
  61. package/dist/policy/vocabulary.js +20 -0
  62. package/dist/policy/vocabulary.js.map +1 -1
  63. package/dist/report/sarif.d.ts.map +1 -1
  64. package/dist/report/sarif.js +10 -0
  65. package/dist/report/sarif.js.map +1 -1
  66. package/dist/report/summary.d.ts.map +1 -1
  67. package/dist/report/summary.js +2 -0
  68. package/dist/report/summary.js.map +1 -1
  69. package/dist/report/text.d.ts.map +1 -1
  70. package/dist/report/text.js +5 -0
  71. package/dist/report/text.js.map +1 -1
  72. package/package.json +4 -3
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # frostjs
2
2
 
3
+ [![ci](https://github.com/keithadler/frostjs/actions/workflows/ci.yml/badge.svg)](https://github.com/keithadler/frostjs/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/%40keithadler%2Ffrostjs)](https://www.npmjs.com/package/@keithadler/frostjs)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
3
7
  **The model wrote it. Did anyone decide it could do that?**
4
8
 
5
9
  frostjs is a deny-by-default capability gate for JavaScript. You write a
@@ -141,14 +145,16 @@ flags are in [SHOWCASE.md](SHOWCASE.md).
141
145
 
142
146
  ## Status
143
147
 
144
- Pre-alpha, feature complete against [REQUIREMENTS.md](REQUIREMENTS.md): all
145
- eight capability families, frost-dialect policies, scope analysis,
146
- baselines, changed-lines mode, json/sarif/github output, a GitHub Action,
147
- an ESLint plugin, a fingerprint registry for vendored code with SRI output,
148
- TypeScript, JSX and inline HTML. Run on Excalidraw (656 files, a
149
- TypeScript and React monorepo) it finishes in under a second and
150
- `frostjs init` writes an 18-line policy; every finding was checked by hand.
151
- Not yet published to npm.
148
+ Published as [`@keithadler/frostjs`](https://www.npmjs.com/package/@keithadler/frostjs).
149
+ Nine capability families across JavaScript, TypeScript, JSX and HTML (inline
150
+ scripts and attribute surfaces: `on*` handlers, `javascript:` URLs, remote
151
+ `<script src>`). Frost-dialect policies with shared bases (`extends`), scope
152
+ analysis, taint analysis (source -> sink, one hop across functions),
153
+ baselines, changed-lines mode, unused-grant reporting, json/sarif/github
154
+ output, a GitHub Action, an ESLint plugin, and a fingerprint registry for
155
+ vendored code with SRI output. Run on Excalidraw (656 files, a TypeScript
156
+ and React monorepo) it finishes in under a second and `frostjs init` writes
157
+ an 18-line policy; every finding was checked by hand.
152
158
 
153
159
  ```
154
160
  $ cat frostjs.policy
@@ -185,6 +191,78 @@ code builds at runtime is called out in a hint rather than quietly widened
185
191
  to `may use the network`. For a large codebase with debt you would rather
186
192
  pay down than grant, use `--baseline` instead (below).
187
193
 
194
+ ## Auditing a dependency before you adopt it
195
+
196
+ ```bash
197
+ npx frostjs audit node_modules/some-widget
198
+ ```
199
+
200
+ No policy involved. It prints, alarming things first: **untrusted input
201
+ reaching a dangerous sink** (a URL parameter, `document.cookie`, or a
202
+ `postMessage` payload flowing into `eval`, `innerHTML`, `importScripts`, a
203
+ redirect - real taint analysis, see below); files where code
204
+ generation or script injection meets a network reach (a *remote code
205
+ path*, the shape that found three.js's bundled remote eval); code
206
+ generation from non-constant input, every host reached, hosts merely
207
+ named in strings (a lead, not a finding), service workers, `postMessage`
208
+ to any origin, and the capability counts. `--format json` for tooling.
209
+ Run it on a pull request's new dependency, or on the one you already have
210
+ and never read.
211
+
212
+ See [docs/CAPABILITIES.md](docs/CAPABILITIES.md) for the full taxonomy with
213
+ the policy phrase for each code, or run `frostjs capabilities`.
214
+
215
+ ## Taint: does untrusted input reach a dangerous sink?
216
+
217
+ `frostjs audit` includes a bounded taint analysis. It answers the question
218
+ capability detection cannot: not "can this code `eval`?" but "does a value
219
+ from the URL, a cookie, or a `postMessage` actually flow into `eval`?".
220
+ That is the difference between a capability and a vulnerability.
221
+
222
+ ```js
223
+ const route = location.hash.slice(1);
224
+ document.getElementById("app").innerHTML = "<div>" + route + "</div>";
225
+ // audit: t.js:2 location.hash -> innerHTML
226
+ ```
227
+
228
+ The rule that keeps it honest: **taint survives only through operations
229
+ that provably preserve it** - string methods, URL decoding, `JSON.parse`,
230
+ template concatenation, member access. Any other function call breaks the
231
+ chain, so `innerHTML = DOMPurify.sanitize(x)` is not flagged while
232
+ `innerHTML = x` is.
233
+
234
+ - **Sources**: `location.search` / `.hash` / `.href` / `.pathname`,
235
+ `document.URL` / `.cookie` / `.referrer` / `.baseURI`, `window.name`,
236
+ `URLSearchParams` reads, and a `window` `message` handler's `event.data`.
237
+ - **Sinks**: `eval`, `Function`, `innerHTML` / `outerHTML` / `srcdoc`,
238
+ `insertAdjacentHTML`, `document.write`, `importScripts`, `import()`,
239
+ `setAttribute("on*"/"srcdoc", ...)`, and `location` / `window.open`
240
+ redirects.
241
+ - **One hop across functions**: a tainted argument passed to a local
242
+ function whose parameter reaches a sink is flagged (`setHtml(x) { el.innerHTML = x }`
243
+ called with `location.hash`), reported `... (via setHtml())`. A parameter
244
+ only counts if it reaches the sink through provably-preserving operations,
245
+ so a helper that sanitizes its argument is not a sink.
246
+ - **Limit, stated plainly**: flow is followed one function hop, not a whole
247
+ call graph (a parameter that reaches a sink through a second function is
248
+ not summarized), and DOM input values (`el.value`) are not modeled as
249
+ sources. It finds real flows; it does not claim to find all of them.
250
+
251
+ Run over 21 MB of popular packages it reports zero (mature libraries
252
+ sanitize); on real application code it lights up the flows a reviewer
253
+ would flag by hand.
254
+
255
+ Taint is a human report in `frostjs audit` and, with `--taint`, an
256
+ enforced gate in `frostjs check`: each flow becomes a `taint.<sink>`
257
+ finding that fails the build and appears in the json, sarif and github
258
+ outputs, so it lands as a code-scanning alert on the pull request.
259
+ `--changed-since`, `--baseline` and `// frostjs: ignore[taint]` all apply
260
+ to taint findings, so it adopts on a legacy codebase the same way the
261
+ capability gate does. It is off by default because it is best-effort;
262
+ the capability gate stays deterministic. Put `forbid tainted flows` in
263
+ `frostjs.policy` to turn the gate on for everyone without the flag, so the
264
+ committed policy expresses the whole security posture in one place.
265
+
188
266
  ## Policy files
189
267
 
190
268
  A policy is a `frostjs.policy` file in frost's policy dialect: one rule per
@@ -194,6 +272,7 @@ printed whenever that rule refuses something.
194
272
 
195
273
  ```
196
274
  policy "<name>" optional, once
275
+ extends "<base.frostjs.policy>" merge a base policy first
197
276
  ignore "<glob>", ... files not analyzed at all
198
277
  vendored "<glob>", ... third-party files, checked by fingerprint
199
278
  may use <capability> [in "<glob>", ...] [until YYYY-MM-DD]
@@ -201,6 +280,7 @@ may reach "<host>", ... [in "<glob>", ...] [until YYYY-MM-DD]
201
280
  forbid [using] <capability> [in "<glob>", ...]
202
281
  forbid reaching "<host>", ... [in "<glob>", ...]
203
282
  forbid everything else optional, readability only
283
+ forbid tainted flows gate on taint (like --taint)
204
284
  ```
205
285
 
206
286
  `<capability>` is a phrase or a code. A family name grants the whole family.
@@ -219,8 +299,10 @@ forbid everything else optional, readability only
219
299
  | `html injection` | `dom-escape` |
220
300
  | `identity`, `fingerprinting` | `identity` |
221
301
  | `navigation` | `navigation` |
302
+
222
303
  | `globals` | `globals` |
223
304
  | `workers`, `service workers` | `worker` |
305
+ | `device access`, `file access`, `usb`, `bluetooth`, `notifications` | `device` |
224
306
  | `everything` | `*` |
225
307
 
226
308
  Rules:
@@ -251,6 +333,7 @@ all, every capability is denied and a note says so.
251
333
 
252
334
  ```
253
335
  frostjs init [paths] write a starter frostjs.policy granting what the code does today
336
+ frostjs audit <paths> what the code does, no policy needed: hosts, codegen, script injection, remote code paths
254
337
  frostjs <paths...> discover and analyze .js/.mjs/.cjs/.jsx/.ts/.tsx/.mts/.cts and inline <script> in .html under paths
255
338
  frostjs csp print the Content-Security-Policy header the policy implies
256
339
  frostjs summary print a plain-English reading of the policy
@@ -265,6 +348,7 @@ frostjs --min-confidence <c> lowest confidence that fails: certain, probable (d
265
348
  frostjs --baseline <file> denials recorded in this file do not fail the build
266
349
  frostjs --update-baseline write every current denial into the baseline and exit 0
267
350
  frostjs --changed-since <ref> fail only on uses in lines changed since the git ref
351
+ frostjs --taint also fail on untrusted input reaching a dangerous sink
268
352
  frostjs --format <f> text (default), json, sarif, or github
269
353
  frostjs --version print the version and exit
270
354
  frostjs --help show usage
@@ -432,6 +516,37 @@ For pull-request checks, `frostjs --changed-since origin/main src` fails only
432
516
  on uses that sit in lines the branch added or modified; the rest are counted
433
517
  as "unchanged". Untracked files count as entirely changed.
434
518
 
519
+ ## Tightening a policy
520
+
521
+ `frostjs check --unused src` lists grants that matched nothing on the scan,
522
+ so an over-broad or redundant line can be removed:
523
+
524
+ ```
525
+ 2 grants matched nothing (remove, or scan more):
526
+ frostjs.policy line 3: may use session storage
527
+ frostjs.policy line 5: may use cookies
528
+ ```
529
+
530
+ It prints to stderr and does not change the exit code. Run it on a full
531
+ scan, not a changed-lines subset, or a grant will look unused only because
532
+ the file that needs it was not scanned.
533
+
534
+ ## Shared base policies
535
+
536
+ An organization or monorepo can keep one base policy and extend it:
537
+
538
+ ```
539
+ # packages/widget/frostjs.policy
540
+ extends "../../frostjs.base.policy"
541
+ may use local storage
542
+ ```
543
+
544
+ The base is merged in first, so its grants apply and its `forbid` lines
545
+ (and `forbid tainted flows`) cannot be loosened by a child. Path globs in
546
+ the base are interpreted relative to the base file and rebased when merged,
547
+ so `may use cookies in "legacy/*"` keeps meaning the base's `legacy/`. A
548
+ cycle or a missing base is a precise error naming the line.
549
+
435
550
  ## Suppressing a single use
436
551
 
437
552
  ```js
@@ -464,6 +579,8 @@ to a reviewer; suppression is for the one-off.
464
579
  | `network.eventsource` | `EventSource` |
465
580
  | `network.beacon` | `navigator.sendBeacon` |
466
581
  | `network.import` | dynamic `import()` of an absolute URL or an expression |
582
+ | `network.importscripts` | `importScripts(url)` in a worker (loads and runs a script) |
583
+ | `network.resource` | `el.src = "https://..."` or `setAttribute("src", ...)` naming another host (literal or folded const only) |
467
584
  | `codegen.eval` | `eval` |
468
585
  | `codegen.function` | `Function(...)`, `new Function(...)` |
469
586
  | `codegen.timer` | `setTimeout` / `setInterval` with string code |
@@ -471,6 +588,7 @@ to a reviewer; suppression is for the one-off.
471
588
  | `dom-escape.html` | assignment to `innerHTML` / `outerHTML` / `srcdoc`, `insertAdjacentHTML`, `createContextualFragment`, JSX `dangerouslySetInnerHTML` / `srcdoc` |
472
589
  | `dom-escape.script` | `document.createElement("script")`, JSX `<script>` |
473
590
  | `dom-escape.iframe` | `document.createElement("iframe")`, JSX `<iframe>` |
591
+ | `dom-escape.handler` | `setAttribute("onclick" / "onerror" / ..., code)` (installs a handler from a string) |
474
592
  | `identity.device` | `navigator.userAgent`, `platform`, `vendor`, `plugins`, `hardwareConcurrency`, `deviceMemory`... |
475
593
  | `identity.geolocation` | `navigator.geolocation` |
476
594
  | `identity.media` | `navigator.mediaDevices`, `getUserMedia` |
@@ -481,12 +599,17 @@ to a reviewer; suppression is for the one-off.
481
599
  | `navigation.open` | `window.open` |
482
600
  | `navigation.history` | `history.pushState` / `replaceState` / `back` / `forward` / `go` |
483
601
  | `navigation.postmessage` | `postMessage` to `parent` / `top` / `opener` / `contentWindow`, or with a string origin |
602
+ | `navigation.message-receive` | `window.addEventListener("message", ...)` whose handler reads `event.data` but never checks `event.origin` |
484
603
  | `globals.window` | assignment to `window.*` / `globalThis.*`, `Object.defineProperty(window, ...)` |
485
604
  | `globals.prototype` | assignment to a built-in or its prototype (`Array.prototype.x = `, `Error.prepareStackTrace = `), or `Object.defineProperty` / `assign` on one |
486
605
  | `worker.dedicated` | `new Worker(url)` |
487
606
  | `worker.shared` | `new SharedWorker(url)` |
488
607
  | `worker.service` | `navigator.serviceWorker.register(url)` |
489
608
  | `worker.worklet` | `CSS.paintWorklet.addModule(url)`, `audioWorklet.addModule(url)`... |
609
+ | `device.filesystem` | `showOpenFilePicker` / `showSaveFilePicker` / `showDirectoryPicker` (read/write the user's files) |
610
+ | `device.usb` / `device.bluetooth` / `device.serial` / `device.hid` / `device.midi` | `navigator.usb` etc. (hardware access) |
611
+ | `device.wakelock` | `navigator.wakeLock` |
612
+ | `device.notification` | `Notification` |
490
613
 
491
614
  Each is recognized bare, via `window` / `globalThis` / `self`, and via a
492
615
  computed member whose name is a string literal (`window["localStorage"]`),
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `frostjs audit`: what a body of code does, with no policy involved. The
3
+ * question a reviewer asks of a dependency before adopting it, or of a
4
+ * pull request that adds one: which hosts does it reach, does it generate
5
+ * code from non-constant input, does it inject scripts, does it register a
6
+ * service worker, and do any of those meet in one file.
7
+ *
8
+ * The shape that turned up ECSY's remote eval in three.js is the last one:
9
+ * code generation or script injection with a non-constant argument, plus a
10
+ * network reach, in the same file. It is reported as a "remote code path".
11
+ */
12
+ import type { CapabilityUse } from "./extract/capability.js";
13
+ import type { TaintFinding } from "./extract/taint.js";
14
+ export interface FileAudit {
15
+ file: string;
16
+ /** Code generation whose input is not a constant. */
17
+ dynamicCodegen: CapabilityUse[];
18
+ scriptInjection: CapabilityUse[];
19
+ /** Hosts the engine resolved, excluding the document's own origin. */
20
+ hosts: string[];
21
+ /** Hosts named in URL string literals anywhere in the file. A lead, not a finding. */
22
+ literalHosts: string[];
23
+ unknownDestinations: number;
24
+ /** The file reads the page URL: a switch such a path can be flipped with. */
25
+ readsUrl: boolean;
26
+ /** Emscripten glue, whose code generation and wasm fetch are a known benign shape. */
27
+ emscripten: boolean;
28
+ }
29
+ export interface Audit {
30
+ files: number;
31
+ uses: number;
32
+ /** Distinct capability codes with counts. */
33
+ capabilities: Map<string, number>;
34
+ /** Resolved hosts with use counts, excluding the document's own origin. */
35
+ hosts: Map<string, number>;
36
+ /** Hosts named in string literals but not resolved as a destination. */
37
+ literalHosts: string[];
38
+ dynamicCodegen: CapabilityUse[];
39
+ scriptInjection: CapabilityUse[];
40
+ serviceWorkers: CapabilityUse[];
41
+ wildcardPostMessage: CapabilityUse[];
42
+ /** Files where code generation or script injection meets a network reach. */
43
+ remoteCodePaths: FileAudit[];
44
+ /** Untrusted input (URL, cookie, postMessage) reaching a dangerous sink. The most serious thing here. */
45
+ taintFlows: TaintFinding[];
46
+ }
47
+ /** A codegen use whose input is not a constant: `eval(data.script)`, not `Function("return this")`. */
48
+ export declare function isDynamicCodegen(u: CapabilityUse): boolean;
49
+ /** Hosts at the start of URL-shaped string literals, minus documentation hosts. Comments are not strings. */
50
+ export declare function literalHostsIn(strings: readonly string[]): string[];
51
+ /** Emscripten output: its code generation is embind and its fetch is its own .wasm. Reported, ranked below the rest. */
52
+ export declare const isEmscripten: (text: string) => boolean;
53
+ export interface FileSource {
54
+ /** The file's text, for the reads-the-URL lead. */
55
+ text: string;
56
+ /** Its string literals, for the hosts-named-in-strings lead. */
57
+ strings: readonly string[];
58
+ }
59
+ /**
60
+ * Audit uses grouped by file. `sources` supplies each file's text and
61
+ * string literals for the two leads; a file missing from it just loses
62
+ * those columns.
63
+ */
64
+ export declare function audit(byFile: ReadonlyMap<string, readonly CapabilityUse[]>, sources: ReadonlyMap<string, FileSource>, taintFlows?: readonly TaintFinding[]): Audit;
65
+ /** The audit as a report a person reads top to bottom: the alarming things first. */
66
+ export declare function formatAudit(a: Audit): string;
67
+ /** The audit as JSON. Maps become objects; uses keep their positions. */
68
+ export declare function auditJson(a: Audit): string;
69
+ /** Group uses by file, for audit(). */
70
+ export declare function groupByFile(uses: readonly CapabilityUse[]): Map<string, CapabilityUse[]>;
71
+ //# sourceMappingURL=audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,sEAAsE;IACtE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sFAAsF;IACtF,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;IAClB,sFAAsF;IACtF,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,2EAA2E;IAC3E,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,wEAAwE;IACxE,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,eAAe,EAAE,aAAa,EAAE,CAAC;IACjC,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,mBAAmB,EAAE,aAAa,EAAE,CAAC;IACrC,6EAA6E;IAC7E,eAAe,EAAE,SAAS,EAAE,CAAC;IAC7B,yGAAyG;IACzG,UAAU,EAAE,YAAY,EAAE,CAAC;CAC5B;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAO1D;AAMD,6GAA6G;AAC7G,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAOnE;AAKD,wHAAwH;AACxH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,OAA0E,CAAC;AAEvH,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CACnB,MAAM,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC,EACrD,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,EACxC,UAAU,GAAE,SAAS,YAAY,EAAO,GACvC,KAAK,CAoDP;AAID,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAyD5C;AAED,yEAAyE;AACzE,wBAAgB,SAAS,CAAC,CAAC,EAAE,KAAK,GAAG,MAAM,CAsC1C;AAED,uCAAuC;AACvC,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAOxF"}
package/dist/audit.js ADDED
@@ -0,0 +1,202 @@
1
+ import { SAME_ORIGIN } from "./extract/target.js";
2
+ /** A codegen use whose input is not a constant: `eval(data.script)`, not `Function("return this")`. */
3
+ export function isDynamicCodegen(u) {
4
+ if (!u.capability.startsWith("codegen.") || u.capability === "codegen.write")
5
+ return false;
6
+ const e = u.expression.replace(/\s+/g, " ");
7
+ if (/^(new )?Function\s*\(\s*["'`]return this["'`]\s*\)/.test(e))
8
+ return false;
9
+ if (/^eval$/.test(e))
10
+ return false; // a bare reference: typeof eval, feature detection
11
+ const args = e.replace(/^(new )?(eval|Function|setTimeout|setInterval)\s*\(/, "");
12
+ return !/^\s*["'`][^"'`]*["'`]\s*(,\s*["'`][^"'`]*["'`]\s*)*\)/.test(args);
13
+ }
14
+ /** Hosts that appear only in documentation links inside strings and comments. */
15
+ const DOC_HOSTS = /(^|\.)(w3\.org|mozilla\.org|github\.com|githubusercontent\.com|example\.com|wikipedia\.org|stackoverflow\.com|ietf\.org|json-schema\.org|whatwg\.org|khronos\.org|ecma-international\.org|npmjs\.com)$/i;
16
+ /** Hosts at the start of URL-shaped string literals, minus documentation hosts. Comments are not strings. */
17
+ export function literalHostsIn(strings) {
18
+ const out = new Set();
19
+ for (const s of strings) {
20
+ const m = /^\s*https?:\/\/([a-z0-9.-]+\.[a-z]{2,})(?=[/:?#\s]|$)/i.exec(s);
21
+ if (m && !DOC_HOSTS.test(m[1]))
22
+ out.add(m[1].toLowerCase());
23
+ }
24
+ return [...out].sort();
25
+ }
26
+ const interesting = (t) => t !== null && t !== SAME_ORIGIN && t !== "data:" && t !== "blob:" && t !== "javascript:" && t !== "*";
27
+ /** Emscripten output: its code generation is embind and its fetch is its own .wasm. Reported, ranked below the rest. */
28
+ export const isEmscripten = (text) => /emscripten/i.test(text) && /wasmBinary|WebAssembly/.test(text);
29
+ /**
30
+ * Audit uses grouped by file. `sources` supplies each file's text and
31
+ * string literals for the two leads; a file missing from it just loses
32
+ * those columns.
33
+ */
34
+ export function audit(byFile, sources, taintFlows = []) {
35
+ const capabilities = new Map();
36
+ const hosts = new Map();
37
+ const literal = new Set();
38
+ const dynamicCodegen = [];
39
+ const scriptInjection = [];
40
+ const serviceWorkers = [];
41
+ const wildcardPostMessage = [];
42
+ const remoteCodePaths = [];
43
+ let uses = 0;
44
+ for (const [file, fileUses] of byFile) {
45
+ uses += fileUses.length;
46
+ const { text, strings } = sources.get(file) ?? { text: "", strings: [] };
47
+ const fa = {
48
+ file,
49
+ dynamicCodegen: fileUses.filter(isDynamicCodegen),
50
+ scriptInjection: fileUses.filter((u) => u.capability === "dom-escape.script"),
51
+ hosts: [...new Set(fileUses.map((u) => u.target).filter(interesting))].sort(),
52
+ literalHosts: literalHostsIn(strings),
53
+ unknownDestinations: fileUses.filter((u) => u.capability.startsWith("network.") && u.target === null).length,
54
+ readsUrl: /URLSearchParams|location\.search|location\.hash/.test(text),
55
+ emscripten: isEmscripten(text),
56
+ };
57
+ for (const u of fileUses) {
58
+ capabilities.set(u.capability, (capabilities.get(u.capability) ?? 0) + 1);
59
+ if (interesting(u.target))
60
+ hosts.set(u.target, (hosts.get(u.target) ?? 0) + 1);
61
+ if (u.capability === "worker.service")
62
+ serviceWorkers.push(u);
63
+ if (u.capability === "navigation.postmessage" && u.target === "*")
64
+ wildcardPostMessage.push(u);
65
+ }
66
+ for (const h of fa.literalHosts)
67
+ literal.add(h);
68
+ dynamicCodegen.push(...fa.dynamicCodegen);
69
+ scriptInjection.push(...fa.scriptInjection);
70
+ const remoteCode = fa.dynamicCodegen.length > 0 || fa.scriptInjection.length > 0;
71
+ const reaches = fa.hosts.length > 0 || fa.literalHosts.length > 0 || fa.unknownDestinations > 0;
72
+ if (remoteCode && reaches)
73
+ remoteCodePaths.push(fa);
74
+ }
75
+ remoteCodePaths.sort((x, y) => Number(x.emscripten) - Number(y.emscripten));
76
+ const literalHosts = [...literal].filter((h) => !hosts.has(h)).sort();
77
+ return {
78
+ files: byFile.size,
79
+ uses,
80
+ capabilities,
81
+ hosts,
82
+ literalHosts,
83
+ dynamicCodegen,
84
+ scriptInjection,
85
+ serviceWorkers,
86
+ wildcardPostMessage,
87
+ remoteCodePaths,
88
+ taintFlows: [...taintFlows],
89
+ };
90
+ }
91
+ const site = (u) => `${u.file}:${u.line}: ${u.expression.replace(/\s+/g, " ").slice(0, 90)}`;
92
+ /** The audit as a report a person reads top to bottom: the alarming things first. */
93
+ export function formatAudit(a) {
94
+ const lines = [];
95
+ const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
96
+ lines.push(`${plural(a.files, "file")}, ${plural(a.uses, "capability use")}`);
97
+ lines.push("");
98
+ lines.push("untrusted input reaching a dangerous sink:");
99
+ if (a.taintFlows.length === 0)
100
+ lines.push(" none");
101
+ for (const t of a.taintFlows)
102
+ lines.push(` ${t.file}:${t.line}: ${t.source} -> ${t.sink}: ${t.expression}`);
103
+ lines.push("");
104
+ lines.push("remote code paths (code generation or script injection meets a network reach in one file):");
105
+ if (a.remoteCodePaths.length === 0)
106
+ lines.push(" none");
107
+ for (const f of a.remoteCodePaths) {
108
+ const tags = [f.readsUrl ? "reads the page URL" : "", f.emscripten ? "Emscripten glue" : ""].filter(Boolean);
109
+ lines.push(` ${f.file}${tags.length ? ` [${tags.join(", ")}]` : ""}`);
110
+ for (const u of [...f.dynamicCodegen, ...f.scriptInjection])
111
+ lines.push(` ${site(u)}`);
112
+ const reach = [
113
+ ...f.hosts,
114
+ ...f.literalHosts.filter((h) => !f.hosts.includes(h)).map((h) => `${h} (named in a string)`),
115
+ ];
116
+ if (reach.length)
117
+ lines.push(` reaches: ${reach.join(", ")}`);
118
+ if (f.unknownDestinations)
119
+ lines.push(` and ${plural(f.unknownDestinations, "destination")} that cannot be read`);
120
+ }
121
+ lines.push("");
122
+ lines.push("code generation from non-constant input:");
123
+ const rcFiles = new Set(a.remoteCodePaths.map((f) => f.file));
124
+ const cg = a.dynamicCodegen.filter((u) => !rcFiles.has(u.file));
125
+ if (a.dynamicCodegen.length === 0)
126
+ lines.push(" none");
127
+ else if (cg.length === 0)
128
+ lines.push(" only in the remote code paths above");
129
+ for (const u of cg)
130
+ lines.push(` ${site(u)}`);
131
+ lines.push("");
132
+ lines.push("hosts reached:");
133
+ if (a.hosts.size === 0)
134
+ lines.push(" none resolved");
135
+ for (const [h, n] of [...a.hosts].sort((x, y) => y[1] - x[1]))
136
+ lines.push(` ${h} (${plural(n, "use")})`);
137
+ if (a.literalHosts.length)
138
+ lines.push(` named in strings, not resolved as a destination: ${a.literalHosts.join(", ")}`);
139
+ lines.push("");
140
+ if (a.serviceWorkers.length) {
141
+ lines.push("service workers:");
142
+ for (const u of a.serviceWorkers)
143
+ lines.push(` ${site(u)}`);
144
+ lines.push("");
145
+ }
146
+ if (a.wildcardPostMessage.length) {
147
+ lines.push("postMessage to any origin:");
148
+ for (const u of a.wildcardPostMessage)
149
+ lines.push(` ${site(u)}`);
150
+ lines.push("");
151
+ }
152
+ lines.push("capabilities:");
153
+ if (a.capabilities.size === 0)
154
+ lines.push(" none");
155
+ for (const [c, n] of [...a.capabilities].sort((x, y) => x[0].localeCompare(y[0])))
156
+ lines.push(` ${c} (${n})`);
157
+ return lines.join("\n") + "\n";
158
+ }
159
+ /** The audit as JSON. Maps become objects; uses keep their positions. */
160
+ export function auditJson(a) {
161
+ const uses = (us) => us.map((u) => ({
162
+ file: u.file,
163
+ line: u.line,
164
+ column: u.column,
165
+ capability: u.capability,
166
+ target: u.target,
167
+ expression: u.expression,
168
+ }));
169
+ return (JSON.stringify({
170
+ files: a.files,
171
+ uses: a.uses,
172
+ capabilities: Object.fromEntries(a.capabilities),
173
+ taintFlows: a.taintFlows,
174
+ hosts: Object.fromEntries(a.hosts),
175
+ literalHosts: a.literalHosts,
176
+ dynamicCodegen: uses(a.dynamicCodegen),
177
+ scriptInjection: uses(a.scriptInjection),
178
+ serviceWorkers: uses(a.serviceWorkers),
179
+ wildcardPostMessage: uses(a.wildcardPostMessage),
180
+ remoteCodePaths: a.remoteCodePaths.map((f) => ({
181
+ file: f.file,
182
+ readsUrl: f.readsUrl,
183
+ emscripten: f.emscripten,
184
+ hosts: f.hosts,
185
+ literalHosts: f.literalHosts,
186
+ unknownDestinations: f.unknownDestinations,
187
+ dynamicCodegen: uses(f.dynamicCodegen),
188
+ scriptInjection: uses(f.scriptInjection),
189
+ })),
190
+ }, null, 2) + "\n");
191
+ }
192
+ /** Group uses by file, for audit(). */
193
+ export function groupByFile(uses) {
194
+ const out = new Map();
195
+ for (const u of uses) {
196
+ if (!out.has(u.file))
197
+ out.set(u.file, []);
198
+ out.get(u.file).push(u);
199
+ }
200
+ return out;
201
+ }
202
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /** One line per family: what the family is about. */
2
+ export declare const FAMILY_SUMMARY: Readonly<Record<string, string>>;
3
+ /** What triggers each member code. Keep terse; no trailing period needed. */
4
+ export declare const CODE_TRIGGER: Readonly<Record<string, string>>;
5
+ export interface CapabilityDoc {
6
+ family: string;
7
+ familySummary: string;
8
+ members: {
9
+ code: string;
10
+ trigger: string;
11
+ phrases: string[];
12
+ }[];
13
+ }
14
+ /** The taxonomy grouped by family, in family order. */
15
+ export declare function capabilityDocs(): CapabilityDoc[];
16
+ /** `frostjs capabilities` as a plain-text listing. */
17
+ export declare function capabilitiesText(): string;
18
+ /** `frostjs capabilities --format json`. */
19
+ export declare function capabilitiesJson(): string;
20
+ /** docs/CAPABILITIES.md, generated so it never drifts from the code. */
21
+ export declare function capabilitiesMarkdown(): string;
22
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../src/capabilities.ts"],"names":[],"mappings":"AASA,qDAAqD;AACrD,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAU3D,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiDzD,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACjE;AAUD,uDAAuD;AACvD,wBAAgB,cAAc,IAAI,aAAa,EAAE,CAUhD;AAED,sDAAsD;AACtD,wBAAgB,gBAAgB,IAAI,MAAM,CAazC;AAED,4CAA4C;AAC5C,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,IAAI,MAAM,CA2B7C"}