@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.
- package/README.md +131 -8
- package/dist/audit.d.ts +71 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +202 -0
- package/dist/audit.js.map +1 -0
- package/dist/capabilities.d.ts +22 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +140 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cli/args.d.ts +5 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +29 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +113 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/extract/html.d.ts +11 -0
- package/dist/extract/html.d.ts.map +1 -1
- package/dist/extract/html.js +58 -0
- package/dist/extract/html.js.map +1 -1
- package/dist/extract/index.d.ts +2 -0
- package/dist/extract/index.d.ts.map +1 -1
- package/dist/extract/index.js +19 -0
- package/dist/extract/index.js.map +1 -1
- package/dist/extract/recognizers/device.d.ts +10 -0
- package/dist/extract/recognizers/device.d.ts.map +1 -0
- package/dist/extract/recognizers/device.js +46 -0
- package/dist/extract/recognizers/device.js.map +1 -0
- package/dist/extract/recognizers/dom-escape.d.ts.map +1 -1
- package/dist/extract/recognizers/dom-escape.js +11 -0
- package/dist/extract/recognizers/dom-escape.js.map +1 -1
- package/dist/extract/recognizers/message.d.ts +16 -0
- package/dist/extract/recognizers/message.d.ts.map +1 -0
- package/dist/extract/recognizers/message.js +92 -0
- package/dist/extract/recognizers/message.js.map +1 -0
- package/dist/extract/recognizers/network.d.ts +10 -2
- package/dist/extract/recognizers/network.d.ts.map +1 -1
- package/dist/extract/recognizers/network.js +49 -6
- package/dist/extract/recognizers/network.js.map +1 -1
- package/dist/extract/taint.d.ts +36 -0
- package/dist/extract/taint.d.ts.map +1 -0
- package/dist/extract/taint.js +503 -0
- package/dist/extract/taint.js.map +1 -0
- package/dist/extract/target.d.ts.map +1 -1
- package/dist/extract/target.js +23 -1
- package/dist/extract/target.js.map +1 -1
- package/dist/policy/compile.d.ts +3 -1
- package/dist/policy/compile.d.ts.map +1 -1
- package/dist/policy/compile.js +1 -0
- package/dist/policy/compile.js.map +1 -1
- package/dist/policy/config.d.ts.map +1 -1
- package/dist/policy/config.js +48 -1
- package/dist/policy/config.js.map +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/parse.d.ts +7 -0
- package/dist/policy/parse.d.ts.map +1 -1
- package/dist/policy/parse.js +24 -1
- package/dist/policy/parse.js.map +1 -1
- package/dist/policy/vocabulary.d.ts.map +1 -1
- package/dist/policy/vocabulary.js +20 -0
- package/dist/policy/vocabulary.js.map +1 -1
- package/dist/report/sarif.d.ts.map +1 -1
- package/dist/report/sarif.js +10 -0
- package/dist/report/sarif.js.map +1 -1
- package/dist/report/summary.d.ts.map +1 -1
- package/dist/report/summary.js +2 -0
- package/dist/report/summary.js.map +1 -1
- package/dist/report/text.d.ts.map +1 -1
- package/dist/report/text.js +5 -0
- package/dist/report/text.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frostjs
|
|
2
2
|
|
|
3
|
+
[](https://github.com/keithadler/frostjs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@keithadler/frostjs)
|
|
5
|
+
[](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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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"]`),
|
package/dist/audit.d.ts
ADDED
|
@@ -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"}
|