@keithadler/frostjs 0.1.0 → 0.3.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 (66) hide show
  1. package/README.md +98 -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 +4 -3
  11. package/dist/cli/args.d.ts.map +1 -1
  12. package/dist/cli/args.js +23 -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 +99 -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/index.js +1 -1
  52. package/dist/policy/index.js.map +1 -1
  53. package/dist/policy/parse.d.ts +2 -0
  54. package/dist/policy/parse.d.ts.map +1 -1
  55. package/dist/policy/parse.js +11 -1
  56. package/dist/policy/parse.js.map +1 -1
  57. package/dist/policy/vocabulary.d.ts.map +1 -1
  58. package/dist/policy/vocabulary.js +20 -0
  59. package/dist/policy/vocabulary.js.map +1 -1
  60. package/dist/report/summary.d.ts.map +1 -1
  61. package/dist/report/summary.js +2 -0
  62. package/dist/report/summary.js.map +1 -1
  63. package/dist/report/text.d.ts.map +1 -1
  64. package/dist/report/text.js +5 -0
  65. package/dist/report/text.js.map +1 -1
  66. 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,15 @@ 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
150
+ markup, frost-dialect policies, scope analysis, taint analysis (source ->
151
+ sink, one hop across functions), baselines, changed-lines mode,
152
+ json/sarif/github output, a GitHub Action, an ESLint plugin, a fingerprint
153
+ registry for vendored code with SRI output, TypeScript, JSX and HTML (inline scripts and attribute surfaces: `on*`
154
+ handlers, `javascript:` URLs, remote `<script src>`). Run on Excalidraw (656 files, a TypeScript and React monorepo) it
155
+ finishes in under a second and `frostjs init` writes an 18-line policy;
156
+ every finding was checked by hand.
152
157
 
153
158
  ```
154
159
  $ cat frostjs.policy
@@ -185,6 +190,78 @@ code builds at runtime is called out in a hint rather than quietly widened
185
190
  to `may use the network`. For a large codebase with debt you would rather
186
191
  pay down than grant, use `--baseline` instead (below).
187
192
 
193
+ ## Auditing a dependency before you adopt it
194
+
195
+ ```bash
196
+ npx frostjs audit node_modules/some-widget
197
+ ```
198
+
199
+ No policy involved. It prints, alarming things first: **untrusted input
200
+ reaching a dangerous sink** (a URL parameter, `document.cookie`, or a
201
+ `postMessage` payload flowing into `eval`, `innerHTML`, `importScripts`, a
202
+ redirect - real taint analysis, see below); files where code
203
+ generation or script injection meets a network reach (a *remote code
204
+ path*, the shape that found three.js's bundled remote eval); code
205
+ generation from non-constant input, every host reached, hosts merely
206
+ named in strings (a lead, not a finding), service workers, `postMessage`
207
+ to any origin, and the capability counts. `--format json` for tooling.
208
+ Run it on a pull request's new dependency, or on the one you already have
209
+ and never read.
210
+
211
+ See [docs/CAPABILITIES.md](docs/CAPABILITIES.md) for the full taxonomy with
212
+ the policy phrase for each code, or run `frostjs capabilities`.
213
+
214
+ ## Taint: does untrusted input reach a dangerous sink?
215
+
216
+ `frostjs audit` includes a bounded taint analysis. It answers the question
217
+ capability detection cannot: not "can this code `eval`?" but "does a value
218
+ from the URL, a cookie, or a `postMessage` actually flow into `eval`?".
219
+ That is the difference between a capability and a vulnerability.
220
+
221
+ ```js
222
+ const route = location.hash.slice(1);
223
+ document.getElementById("app").innerHTML = "<div>" + route + "</div>";
224
+ // audit: t.js:2 location.hash -> innerHTML
225
+ ```
226
+
227
+ The rule that keeps it honest: **taint survives only through operations
228
+ that provably preserve it** - string methods, URL decoding, `JSON.parse`,
229
+ template concatenation, member access. Any other function call breaks the
230
+ chain, so `innerHTML = DOMPurify.sanitize(x)` is not flagged while
231
+ `innerHTML = x` is.
232
+
233
+ - **Sources**: `location.search` / `.hash` / `.href` / `.pathname`,
234
+ `document.URL` / `.cookie` / `.referrer` / `.baseURI`, `window.name`,
235
+ `URLSearchParams` reads, and a `window` `message` handler's `event.data`.
236
+ - **Sinks**: `eval`, `Function`, `innerHTML` / `outerHTML` / `srcdoc`,
237
+ `insertAdjacentHTML`, `document.write`, `importScripts`, `import()`,
238
+ `setAttribute("on*"/"srcdoc", ...)`, and `location` / `window.open`
239
+ redirects.
240
+ - **One hop across functions**: a tainted argument passed to a local
241
+ function whose parameter reaches a sink is flagged (`setHtml(x) { el.innerHTML = x }`
242
+ called with `location.hash`), reported `... (via setHtml())`. A parameter
243
+ only counts if it reaches the sink through provably-preserving operations,
244
+ so a helper that sanitizes its argument is not a sink.
245
+ - **Limit, stated plainly**: flow is followed one function hop, not a whole
246
+ call graph (a parameter that reaches a sink through a second function is
247
+ not summarized), and DOM input values (`el.value`) are not modeled as
248
+ sources. It finds real flows; it does not claim to find all of them.
249
+
250
+ Run over 21 MB of popular packages it reports zero (mature libraries
251
+ sanitize); on real application code it lights up the flows a reviewer
252
+ would flag by hand.
253
+
254
+ Taint is a human report in `frostjs audit` and, with `--taint`, an
255
+ enforced gate in `frostjs check`: each flow becomes a `taint.<sink>`
256
+ finding that fails the build and appears in the json, sarif and github
257
+ outputs, so it lands as a code-scanning alert on the pull request.
258
+ `--changed-since`, `--baseline` and `// frostjs: ignore[taint]` all apply
259
+ to taint findings, so it adopts on a legacy codebase the same way the
260
+ capability gate does. It is off by default because it is best-effort;
261
+ the capability gate stays deterministic. Put `forbid tainted flows` in
262
+ `frostjs.policy` to turn the gate on for everyone without the flag, so the
263
+ committed policy expresses the whole security posture in one place.
264
+
188
265
  ## Policy files
189
266
 
190
267
  A policy is a `frostjs.policy` file in frost's policy dialect: one rule per
@@ -201,6 +278,7 @@ may reach "<host>", ... [in "<glob>", ...] [until YYYY-MM-DD]
201
278
  forbid [using] <capability> [in "<glob>", ...]
202
279
  forbid reaching "<host>", ... [in "<glob>", ...]
203
280
  forbid everything else optional, readability only
281
+ forbid tainted flows gate on taint (like --taint)
204
282
  ```
205
283
 
206
284
  `<capability>` is a phrase or a code. A family name grants the whole family.
@@ -219,8 +297,10 @@ forbid everything else optional, readability only
219
297
  | `html injection` | `dom-escape` |
220
298
  | `identity`, `fingerprinting` | `identity` |
221
299
  | `navigation` | `navigation` |
300
+
222
301
  | `globals` | `globals` |
223
302
  | `workers`, `service workers` | `worker` |
303
+ | `device access`, `file access`, `usb`, `bluetooth`, `notifications` | `device` |
224
304
  | `everything` | `*` |
225
305
 
226
306
  Rules:
@@ -251,6 +331,7 @@ all, every capability is denied and a note says so.
251
331
 
252
332
  ```
253
333
  frostjs init [paths] write a starter frostjs.policy granting what the code does today
334
+ frostjs audit <paths> what the code does, no policy needed: hosts, codegen, script injection, remote code paths
254
335
  frostjs <paths...> discover and analyze .js/.mjs/.cjs/.jsx/.ts/.tsx/.mts/.cts and inline <script> in .html under paths
255
336
  frostjs csp print the Content-Security-Policy header the policy implies
256
337
  frostjs summary print a plain-English reading of the policy
@@ -265,6 +346,7 @@ frostjs --min-confidence <c> lowest confidence that fails: certain, probable (d
265
346
  frostjs --baseline <file> denials recorded in this file do not fail the build
266
347
  frostjs --update-baseline write every current denial into the baseline and exit 0
267
348
  frostjs --changed-since <ref> fail only on uses in lines changed since the git ref
349
+ frostjs --taint also fail on untrusted input reaching a dangerous sink
268
350
  frostjs --format <f> text (default), json, sarif, or github
269
351
  frostjs --version print the version and exit
270
352
  frostjs --help show usage
@@ -464,6 +546,8 @@ to a reviewer; suppression is for the one-off.
464
546
  | `network.eventsource` | `EventSource` |
465
547
  | `network.beacon` | `navigator.sendBeacon` |
466
548
  | `network.import` | dynamic `import()` of an absolute URL or an expression |
549
+ | `network.importscripts` | `importScripts(url)` in a worker (loads and runs a script) |
550
+ | `network.resource` | `el.src = "https://..."` or `setAttribute("src", ...)` naming another host (literal or folded const only) |
467
551
  | `codegen.eval` | `eval` |
468
552
  | `codegen.function` | `Function(...)`, `new Function(...)` |
469
553
  | `codegen.timer` | `setTimeout` / `setInterval` with string code |
@@ -471,6 +555,7 @@ to a reviewer; suppression is for the one-off.
471
555
  | `dom-escape.html` | assignment to `innerHTML` / `outerHTML` / `srcdoc`, `insertAdjacentHTML`, `createContextualFragment`, JSX `dangerouslySetInnerHTML` / `srcdoc` |
472
556
  | `dom-escape.script` | `document.createElement("script")`, JSX `<script>` |
473
557
  | `dom-escape.iframe` | `document.createElement("iframe")`, JSX `<iframe>` |
558
+ | `dom-escape.handler` | `setAttribute("onclick" / "onerror" / ..., code)` (installs a handler from a string) |
474
559
  | `identity.device` | `navigator.userAgent`, `platform`, `vendor`, `plugins`, `hardwareConcurrency`, `deviceMemory`... |
475
560
  | `identity.geolocation` | `navigator.geolocation` |
476
561
  | `identity.media` | `navigator.mediaDevices`, `getUserMedia` |
@@ -481,12 +566,17 @@ to a reviewer; suppression is for the one-off.
481
566
  | `navigation.open` | `window.open` |
482
567
  | `navigation.history` | `history.pushState` / `replaceState` / `back` / `forward` / `go` |
483
568
  | `navigation.postmessage` | `postMessage` to `parent` / `top` / `opener` / `contentWindow`, or with a string origin |
569
+ | `navigation.message-receive` | `window.addEventListener("message", ...)` whose handler reads `event.data` but never checks `event.origin` |
484
570
  | `globals.window` | assignment to `window.*` / `globalThis.*`, `Object.defineProperty(window, ...)` |
485
571
  | `globals.prototype` | assignment to a built-in or its prototype (`Array.prototype.x = `, `Error.prepareStackTrace = `), or `Object.defineProperty` / `assign` on one |
486
572
  | `worker.dedicated` | `new Worker(url)` |
487
573
  | `worker.shared` | `new SharedWorker(url)` |
488
574
  | `worker.service` | `navigator.serviceWorker.register(url)` |
489
575
  | `worker.worklet` | `CSS.paintWorklet.addModule(url)`, `audioWorklet.addModule(url)`... |
576
+ | `device.filesystem` | `showOpenFilePicker` / `showSaveFilePicker` / `showDirectoryPicker` (read/write the user's files) |
577
+ | `device.usb` / `device.bluetooth` / `device.serial` / `device.hid` / `device.midi` | `navigator.usb` etc. (hardware access) |
578
+ | `device.wakelock` | `navigator.wakeLock` |
579
+ | `device.notification` | `Notification` |
490
580
 
491
581
  Each is recognized bare, via `window` / `globalThis` / `self`, and via a
492
582
  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"}
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The canonical capability taxonomy with human descriptions. This is the
3
+ * single source of truth: the README table, the `frostjs capabilities`
4
+ * command and docs/CAPABILITIES.md are all generated from it, and a test
5
+ * fails if it drifts from the codes the extractor actually emits
6
+ * (MEMBER_CODES) or from the families (FAMILIES).
7
+ */
8
+ import { FAMILIES, MEMBER_CODES, CAPABILITY_PHRASES } from "./policy/vocabulary.js";
9
+ /** One line per family: what the family is about. */
10
+ export const FAMILY_SUMMARY = {
11
+ storage: "Persisting data in the browser.",
12
+ network: "Reaching another host, or loading code from one.",
13
+ codegen: "Turning a string into running code.",
14
+ "dom-escape": "Turning a string into live markup, or creating an element that runs code.",
15
+ identity: "Reading who or where the user is.",
16
+ navigation: "Moving the page, or talking to another window.",
17
+ globals: "Mutating shared global state that every script sees.",
18
+ worker: "Running code off the main thread, or intercepting requests.",
19
+ device: "Reaching the machine: hardware, files, notifications.",
20
+ };
21
+ /** What triggers each member code. Keep terse; no trailing period needed. */
22
+ export const CODE_TRIGGER = {
23
+ "storage.local": "localStorage",
24
+ "storage.session": "sessionStorage",
25
+ "storage.indexeddb": "indexedDB",
26
+ "storage.cache": "caches (the Cache API)",
27
+ "storage.cookie": "document.cookie",
28
+ "storage.navigator": "navigator.storage",
29
+ "network.fetch": "fetch(url)",
30
+ "network.xhr": "new XMLHttpRequest()",
31
+ "network.websocket": "new WebSocket(url)",
32
+ "network.eventsource": "new EventSource(url)",
33
+ "network.beacon": "navigator.sendBeacon(url)",
34
+ "network.import": "dynamic import() of an absolute URL or an unresolvable expression",
35
+ "network.importscripts": "importScripts(url) in a worker (loads and runs a script)",
36
+ "network.resource": 'el.src = "https://..." to another host',
37
+ "codegen.eval": "eval(...)",
38
+ "codegen.function": "Function(...) or new Function(...)",
39
+ "codegen.timer": "setTimeout / setInterval with a string first argument",
40
+ "codegen.write": "document.write / document.writeln",
41
+ "dom-escape.html": "assignment to innerHTML / outerHTML / srcdoc, insertAdjacentHTML, createContextualFragment",
42
+ "dom-escape.script": 'document.createElement("script"), JSX <script>',
43
+ "dom-escape.iframe": 'document.createElement("iframe"), JSX <iframe>',
44
+ "dom-escape.handler": 'setAttribute("onclick" / "onerror" / ..., code) (a handler from a string)',
45
+ "identity.device": "navigator.userAgent, platform, plugins, hardwareConcurrency, deviceMemory...",
46
+ "identity.geolocation": "navigator.geolocation",
47
+ "identity.media": "navigator.mediaDevices, getUserMedia",
48
+ "identity.clipboard": 'navigator.clipboard, document.execCommand("copy" / "paste")',
49
+ "identity.credentials": "navigator.credentials",
50
+ "identity.permissions": "navigator.permissions",
51
+ "navigation.location": "assignment to location / location.href, location.assign / replace / reload",
52
+ "navigation.open": "window.open",
53
+ "navigation.history": "history.pushState / replaceState / back / forward / go",
54
+ "navigation.postmessage": "postMessage to parent / top / opener / contentWindow, or with a string origin",
55
+ "navigation.message-receive": 'window.addEventListener("message", ...) whose handler reads event.data but never checks event.origin',
56
+ "globals.window": "assignment to window.* / globalThis.*, Object.defineProperty(window, ...)",
57
+ "globals.prototype": "assignment to a built-in or its prototype, or Object.defineProperty / assign on one",
58
+ "worker.dedicated": "new Worker(url)",
59
+ "worker.shared": "new SharedWorker(url)",
60
+ "worker.service": "navigator.serviceWorker.register(url)",
61
+ "worker.worklet": "CSS.paintWorklet.addModule(url), audioWorklet.addModule(url)...",
62
+ "device.filesystem": "showOpenFilePicker / showSaveFilePicker / showDirectoryPicker (read or write the user's files)",
63
+ "device.usb": "navigator.usb (WebUSB)",
64
+ "device.bluetooth": "navigator.bluetooth (Web Bluetooth)",
65
+ "device.serial": "navigator.serial (Web Serial)",
66
+ "device.hid": "navigator.hid (WebHID)",
67
+ "device.midi": "navigator.requestMIDIAccess (Web MIDI)",
68
+ "device.wakelock": "navigator.wakeLock",
69
+ "device.notification": "Notification",
70
+ };
71
+ /** The phrases a policy author may write for a code (from the vocabulary), sorted shortest first. */
72
+ function phrasesFor(code) {
73
+ return [...CAPABILITY_PHRASES.entries()]
74
+ .filter(([, c]) => c === code)
75
+ .map(([phrase]) => phrase)
76
+ .sort((a, b) => a.length - b.length);
77
+ }
78
+ /** The taxonomy grouped by family, in family order. */
79
+ export function capabilityDocs() {
80
+ return FAMILIES.map((family) => ({
81
+ family,
82
+ familySummary: FAMILY_SUMMARY[family] ?? "",
83
+ members: MEMBER_CODES.filter((c) => c === family || c.startsWith(`${family}.`)).map((code) => ({
84
+ code,
85
+ trigger: CODE_TRIGGER[code] ?? "",
86
+ phrases: phrasesFor(code),
87
+ })),
88
+ }));
89
+ }
90
+ /** `frostjs capabilities` as a plain-text listing. */
91
+ export function capabilitiesText() {
92
+ const lines = [];
93
+ for (const fam of capabilityDocs()) {
94
+ const famPhrases = phrasesFor(fam.family);
95
+ lines.push(`${fam.family}${famPhrases.length ? ` (${famPhrases.join(", ")})` : ""} - ${fam.familySummary}`);
96
+ for (const m of fam.members)
97
+ lines.push(` ${m.code.padEnd(26)} ${m.trigger}`);
98
+ lines.push("");
99
+ }
100
+ lines.push("A policy grants a family or a code:");
101
+ lines.push(" may use storage grants every storage.* member");
102
+ lines.push(" may use local storage grants storage.local");
103
+ lines.push(' may reach "api.example.com" grants the network family to that host');
104
+ return lines.join("\n") + "\n";
105
+ }
106
+ /** `frostjs capabilities --format json`. */
107
+ export function capabilitiesJson() {
108
+ return JSON.stringify({ families: capabilityDocs() }, null, 2) + "\n";
109
+ }
110
+ /** docs/CAPABILITIES.md, generated so it never drifts from the code. */
111
+ export function capabilitiesMarkdown() {
112
+ const lines = [
113
+ "# Capabilities",
114
+ "",
115
+ "The full taxonomy frostjs recognizes. This file is generated from",
116
+ "`src/capabilities.ts` by `frostjs capabilities --format md`; a test fails",
117
+ "if it drifts. A policy grants a whole family (`may use storage`) or a",
118
+ 'single code (`may use local storage`); `may reach "<host>"` grants the',
119
+ "network family to named hosts.",
120
+ "",
121
+ ];
122
+ for (const fam of capabilityDocs()) {
123
+ const famPhrases = phrasesFor(fam.family);
124
+ lines.push(`## ${fam.family}`);
125
+ lines.push("");
126
+ lines.push(fam.familySummary);
127
+ if (famPhrases.length)
128
+ lines.push(`\nPolicy phrase: ${famPhrases.map((p) => `\`${p}\``).join(", ")}.`);
129
+ lines.push("");
130
+ lines.push("| code | triggered by | policy phrase |");
131
+ lines.push("| --- | --- | --- |");
132
+ for (const m of fam.members) {
133
+ const phrase = m.phrases.length ? m.phrases.map((p) => `\`${p}\``).join(", ") : `\`${m.code}\``;
134
+ lines.push(`| \`${m.code}\` | ${m.trigger} | ${phrase} |`);
135
+ }
136
+ lines.push("");
137
+ }
138
+ return lines.join("\n");
139
+ }
140
+ //# sourceMappingURL=capabilities.js.map