@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.
- package/README.md +98 -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 +4 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +99 -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/index.js +1 -1
- package/dist/policy/index.js.map +1 -1
- package/dist/policy/parse.d.ts +2 -0
- package/dist/policy/parse.d.ts.map +1 -1
- package/dist/policy/parse.js +11 -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/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,15 @@ 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
|
|
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"]`),
|
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"}
|
|
@@ -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
|