@ai-ecoverse/slicc-browsers 1.6.0 → 1.7.1
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 +126 -0
- package/fixture-server.mjs +129 -0
- package/guard-runner.mjs +1056 -0
- package/guard-vectors.json +4067 -276
- package/package.json +16 -7
- package/schema/guard-vectors.schema.json +1108 -2
package/README.md
CHANGED
|
@@ -237,6 +237,132 @@ It exports:
|
|
|
237
237
|
- every `{name}` is in `placeholders`;
|
|
238
238
|
- no URL uses a port from 9222 to 9229.
|
|
239
239
|
|
|
240
|
+
## Reference runner
|
|
241
|
+
|
|
242
|
+
`guard-runner.mjs` is dependency-free browser ESM. `run(set, options)` returns
|
|
243
|
+
`{ results, summary(metadata) }`; the module also exports `summary`. Results have
|
|
244
|
+
`id`, `target`, `variant`, `control`, `guarded`, `live`, boolean observations and
|
|
245
|
+
errors. Statuses are `pass`, `fail`, `skip` and `not-run`. Text vectors are reported
|
|
246
|
+
as `not-run`; they are not silently certified. A missing observation check is an
|
|
247
|
+
error before execution.
|
|
248
|
+
|
|
249
|
+
Supply a CDP client with `send(method, params, sessionId): Promise<object>`,
|
|
250
|
+
`waitEvent(predicate, milliseconds): Promise<event>` and an append-only `events`
|
|
251
|
+
array of `{ method, params, sessionId }`. Rejections should expose the protocol
|
|
252
|
+
error as `error.cdp`, or an error message. `client` carries the guarded leg;
|
|
253
|
+
`unguarded`, when supplied, carries unguarded controls and observation reads.
|
|
254
|
+
There is no fallback from observer reads to the guarded client. A bare client
|
|
255
|
+
means guarded transport only; rows requiring an observer report
|
|
256
|
+
`skip` with `not-supported:unguarded-observer`, counted in `summary().observerSkipped`.
|
|
257
|
+
`clientFactory({ variant, detachDelay, vector, target, slot })` is called per row
|
|
258
|
+
and may return either a client or `{ client, unguarded, close, transportVariant }`.
|
|
259
|
+
The factory can configure the guard's own transport delay from this metadata;
|
|
260
|
+
the runner never reaches into the guard. `transportVariant: true` acknowledges
|
|
261
|
+
that configuration, and every result records that boolean (false by default).
|
|
262
|
+
`slot` allows pooling browsers while keeping each row's clients independent.
|
|
263
|
+
The optional `close` releases the row's clients; release pooled browsers after
|
|
264
|
+
`run` completes. Each parallel slot needs an independent browser for
|
|
265
|
+
browser-wide settings. Without a factory execution is serial. Cookie and storage
|
|
266
|
+
rows use fresh browser contexts when `capabilities.browserContexts` is true;
|
|
267
|
+
otherwise they run serially. `capabilities` is an object of booleans or an array
|
|
268
|
+
of supported names. `capabilities.scope` selects owner-specific rows.
|
|
269
|
+
|
|
270
|
+
Map `ordinary`, `ordinary-same-site`, `seven`, `vault`, `vault-x`, `apex`, `own`
|
|
271
|
+
and `client` to origins. Optionally map `plain`, `cross` and `log`; their defaults
|
|
272
|
+
are `ordinary`, `client` and `ordinary`. Origins must preserve the site
|
|
273
|
+
relationships of the data. Explicitly map an unavailable target to `null`: every
|
|
274
|
+
row needing that origin is reported as `skip` for control, guarded and live, with
|
|
275
|
+
reason `not-applicable:no-<target>-origin`; no substitute origin is used and the
|
|
276
|
+
row never passes. A guard whose whole site is protected, such as the extension
|
|
277
|
+
where every `sliccy.ai` host is protected, passes `ordinary-same-site: null`.
|
|
278
|
+
This also applies to origins referenced in setup, triggers, observations and
|
|
279
|
+
liveness checks, not only the row target. Tags and cookie names are unique per leg. `variant`
|
|
280
|
+
selects one variant or an array; omission selects all. `parallel` defaults to 1.
|
|
281
|
+
`log(row)` receives completed rows. A variant supplies the wait before an
|
|
282
|
+
explicit `Target.detachFromTarget` step and for a `settle.variant` step.
|
|
283
|
+
|
|
284
|
+
Every runnable vector has a declarative `liveCheck`; every observation has a
|
|
285
|
+
declarative `check`. Each check declares its `observer`: `fixture` reads request
|
|
286
|
+
logs, `client` reads already-delivered transport data, and `unguarded` requires
|
|
287
|
+
an independent CDP observer. `all` needs the observers for every operand; `any`
|
|
288
|
+
can use an available fixture or client alternative without an unguarded observer.
|
|
289
|
+
Unavailable optional observer alternatives are false, never substituted with guarded reads.
|
|
290
|
+
Checks include: `beacon`, `served`, `header` with
|
|
291
|
+
`on`, `event` with `where`, `result` with `path` and `equals`, `cookie` with `from`,
|
|
292
|
+
`compare`, `refused`, `reached`, `attached`, `delivered`, `title`, `expression`,
|
|
293
|
+
`hops` (each exact nested redirect URL), `detached`, `loaded`, `pre`, `blocked`,
|
|
294
|
+
`registration`, `state` and `filtered`. The diagnostic `executed` and `live`
|
|
295
|
+
checks are not valid liveness proofs.
|
|
296
|
+
Compose checks with `all`, `any` or `not`. Predicates support nested partial
|
|
297
|
+
objects, `$contains`, `$regex`, `$some`, nonempty `$every`, and `$encoded` with
|
|
298
|
+
`{ value, prefix }` for a base64-encoded value with an optional prefix. Reads of titles and
|
|
299
|
+
expressions use the unguarded observer. A `refused` check can declare `refusal.match` to match protocol errors. Transport timeouts, timeout sentinel values and evaluation exceptions fail result,
|
|
300
|
+
comparison and observer-read
|
|
301
|
+
observations; they are never comparable values. Registration refusals must match the
|
|
302
|
+
step's declared expression; dependent observations are omitted and reported by
|
|
303
|
+
the existing result evaluator.
|
|
304
|
+
|
|
305
|
+
`liveCheck` applies only to the guarded leg. Controls are judged by their positive
|
|
306
|
+
observations: a missing or failing effect fails the control. Guarded liveness
|
|
307
|
+
requires the exact target URL in the fixture log or a target-origin `/live`
|
|
308
|
+
heartbeat; alternatively a pre-action `/live?tag=...&step=pre&to=...` request and
|
|
309
|
+
an exact-URL block/error from the client or unguarded frame tree. An error for a
|
|
310
|
+
different URL is not evidence. A declared setup refusal can supply liveness only
|
|
311
|
+
when its `refusal.match` matched. Transport connectivity and command completion
|
|
312
|
+
alone never prove liveness. Cookie and storage rows seed a canary outside the
|
|
313
|
+
guard and read it back outside the guard; refused writes require unchanged
|
|
314
|
+
canary state and a refusal or filtered call response. Their control can clear or
|
|
315
|
+
modify state as required by its positive observations. `C5.auth` and
|
|
316
|
+
`C5.guessed-id.continue-with-auth` use fixture Authorization logs and refusal of
|
|
317
|
+
candidate auth ids; `C5.auth` also checks delivered auth events. Their liveness
|
|
318
|
+
requires an auth request at the fixture, not a completed fetch. Neither uses a
|
|
319
|
+
plain-tab baseline: headless Chromium can leave a credentials-including fetch
|
|
320
|
+
to a Basic 401 pending even without an attached client.
|
|
321
|
+
|
|
322
|
+
Beacon settlement uses only requests recorded after the page action starts. For
|
|
323
|
+
redirect chains it also requires the exact final destination request in the
|
|
324
|
+
fixture log. A setup beacon on the same origin and tag cannot settle an unfinished
|
|
325
|
+
redirect chain. This does not change the observations used to judge either leg.
|
|
326
|
+
|
|
327
|
+
The separately exported Node module `@ai-ecoverse/slicc-browsers/fixture` exposes
|
|
328
|
+
`fixtureServer({ host, port, tls })`, defaulting to a random loopback port.
|
|
329
|
+
Pass `tls: { key, cert }` to serve HTTPS using Node TLS key and certificate
|
|
330
|
+
values. The returned origin, request logs, redirect URLs and document heartbeats
|
|
331
|
+
use `https://<Host>` with TLS, so `served`, `hops` and `liveCheck` compare against
|
|
332
|
+
the mapped HTTPS origins, including an extension client at `https://*.sliccy.ai`.
|
|
333
|
+
Without `tls`, the server and logged URLs remain HTTP. It returns
|
|
334
|
+
`origin`, `port`, `log()`, `reset()` and `close()`. Log entries contain `url`,
|
|
335
|
+
`method`, `headers` and `time`. Mirror that API when using another fixture.
|
|
336
|
+
The server serves document, frame, redirect, CSP, authentication, data, image,
|
|
337
|
+
canary and beacon paths. Configure host resolution in your test browser, not in
|
|
338
|
+
the runner. `npm run test:guard` exercises Chromium twice with no guard, one target per
|
|
339
|
+
invocation: controls must pass and guarded expectations must fail. For a single
|
|
340
|
+
target use `GUARD_TARGET=seven npm run test:guard-target`. The fixture also serves
|
|
341
|
+
`/live` image requests from stand-in documents, independently of injected code.
|
|
342
|
+
|
|
343
|
+
```js
|
|
344
|
+
import set from '@ai-ecoverse/slicc-browsers/guard-vectors.json' with { type: 'json' };
|
|
345
|
+
import { run } from '@ai-ecoverse/slicc-browsers/guard-runner';
|
|
346
|
+
import { fixtureServer } from '@ai-ecoverse/slicc-browsers/fixture';
|
|
347
|
+
const fixture = await fixtureServer();
|
|
348
|
+
const targets = await configureTestOrigins(fixture.port);
|
|
349
|
+
const capabilities = {
|
|
350
|
+
autoAttach: true,
|
|
351
|
+
browserSession: true,
|
|
352
|
+
browserContexts: true,
|
|
353
|
+
};
|
|
354
|
+
const execution = await run(set, {
|
|
355
|
+
clientFactory: (metadata) => connectGuardAndObserver(metadata),
|
|
356
|
+
targets,
|
|
357
|
+
capabilities,
|
|
358
|
+
variant: ['prompt', 'lagging'],
|
|
359
|
+
parallel: 6,
|
|
360
|
+
fixture,
|
|
361
|
+
});
|
|
362
|
+
console.log(execution.summary({ guard: 'my-guard', sha: 'build-sha' }));
|
|
363
|
+
await fixture.close();
|
|
364
|
+
```
|
|
365
|
+
|
|
240
366
|
## The process source
|
|
241
367
|
|
|
242
368
|
The process scan finds browsers started with a debugging port by something else:
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
import { createServer as createSecureServer } from 'node:https';
|
|
3
|
+
|
|
4
|
+
const tall = Buffer.from(
|
|
5
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
6
|
+
'base64'
|
|
7
|
+
);
|
|
8
|
+
const escape = (value) =>
|
|
9
|
+
value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<');
|
|
10
|
+
|
|
11
|
+
function document(url, html) {
|
|
12
|
+
const p = url.searchParams;
|
|
13
|
+
const csp = p.has('csp') || url.pathname === '/csp';
|
|
14
|
+
return html(
|
|
15
|
+
'doc',
|
|
16
|
+
`<p>document ${escape(p.get('n') ?? '')}</p>${csp ? '<script>document.title="inline"; document.body.dataset.inline="true"</script>' : ''}`,
|
|
17
|
+
csp ? { 'content-security-policy': "script-src 'none'" } : {}
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function trigger(p) {
|
|
22
|
+
const kind = p.get('k') ?? '';
|
|
23
|
+
const to = p.get('to') ?? '';
|
|
24
|
+
const frame = kind === 'iframe' ? `<iframe src="${escape(to)}"></iframe>` : '';
|
|
25
|
+
return `<p>trigger</p><button onclick="go()">Go</button>${frame}<script>const to=${JSON.stringify(to)};function go(){const k=${JSON.stringify(kind)};if(k==='href'||k==='deferred')location.href=to;if(k==='replace')location.replace(to);if(k==='popup')window.open(to);if(k==='form'){const u=new URL(to);const f=document.createElement('form');f.action=u.origin+u.pathname;for(const [name,value] of u.searchParams){const i=document.createElement('input');i.type='hidden';i.name=name;i.value=value;f.append(i);}document.body.append(f);f.submit();}return 'went';}${kind === 'deferred' ? `setTimeout(go,${Number(p.get('defer') ?? 3000)});` : ''}</script>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function route(req, url, send, html) {
|
|
29
|
+
const p = url.searchParams;
|
|
30
|
+
const routes = {
|
|
31
|
+
'/beacon': () => send(204, ''),
|
|
32
|
+
'/hop': () =>
|
|
33
|
+
send(Number(p.get('code') ?? 302), 'moved', 'text/plain', { location: p.get('to') ?? '/' }),
|
|
34
|
+
'/data': () => send(200, `real-${p.get('n') ?? ''}`, 'text/plain'),
|
|
35
|
+
'/live': () => send(200, tall, 'image/png'),
|
|
36
|
+
'/img': () => send(200, tall, 'image/png'),
|
|
37
|
+
'/script': () =>
|
|
38
|
+
send(
|
|
39
|
+
200,
|
|
40
|
+
`globalThis.vectorScript=${JSON.stringify(`real-${p.get('n') ?? ''}`)}`,
|
|
41
|
+
'text/javascript'
|
|
42
|
+
),
|
|
43
|
+
'/page': () => html('trigger', trigger(p)),
|
|
44
|
+
'/api/cookies': () => send(200, req.headers.cookie ?? 'none', 'text/plain'),
|
|
45
|
+
'/plant': () =>
|
|
46
|
+
html('plant', '<script>localStorage.setItem("canary","planted")</script>', {
|
|
47
|
+
'set-cookie': [`canary=planted-${url.hostname}; Path=/`, 'del=1; Path=/'],
|
|
48
|
+
}),
|
|
49
|
+
'/rewritten': () => html('rewritten', '<p>REWRITTEN</p>'),
|
|
50
|
+
'/framed': () => {
|
|
51
|
+
const to = p.get('url') ?? 'about:blank';
|
|
52
|
+
const log = p.get('log') ?? url.origin;
|
|
53
|
+
const pre = `${log}/live?tag=${encodeURIComponent(p.get('tag') ?? '')}&step=pre&to=${encodeURIComponent(to)}`;
|
|
54
|
+
return html(
|
|
55
|
+
'framed',
|
|
56
|
+
`<script>navigator.sendBeacon(${JSON.stringify(pre)})</script><iframe src="${escape(to)}"></iframe>`
|
|
57
|
+
);
|
|
58
|
+
},
|
|
59
|
+
'/doc': () => document(url, html),
|
|
60
|
+
'/csp': () => document(url, html),
|
|
61
|
+
};
|
|
62
|
+
if (req.method === 'OPTIONS') return send(204, '');
|
|
63
|
+
if (url.pathname.startsWith('/auth')) {
|
|
64
|
+
if (req.headers.authorization === 'Basic dTpw') return send(200, 'authed', 'text/plain');
|
|
65
|
+
return send(401, 'unauthorized', 'text/plain', {
|
|
66
|
+
'www-authenticate': `Basic realm="${escape(url.pathname)}"`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (url.pathname.startsWith('/canary'))
|
|
70
|
+
return html(
|
|
71
|
+
'canary',
|
|
72
|
+
`<p data-canary="${escape(p.get('value') ?? 'planted')}">canary-marker ${escape(url.host)}</p>`
|
|
73
|
+
);
|
|
74
|
+
const handler = routes[url.pathname];
|
|
75
|
+
if (handler) return handler();
|
|
76
|
+
return send(404, 'missing', 'text/plain');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function fixtureServer({ host = '127.0.0.1', port = 0, tls } = {}) {
|
|
80
|
+
let entries = [];
|
|
81
|
+
const scheme = tls ? 'https' : 'http';
|
|
82
|
+
const handler = (req, res) => {
|
|
83
|
+
const url = new URL(req.url, `${scheme}://${req.headers.host}`);
|
|
84
|
+
entries.push({
|
|
85
|
+
url: url.href,
|
|
86
|
+
method: req.method,
|
|
87
|
+
headers: { ...req.headers },
|
|
88
|
+
time: Date.now(),
|
|
89
|
+
});
|
|
90
|
+
const p = url.searchParams;
|
|
91
|
+
const cors = {
|
|
92
|
+
'access-control-allow-origin': req.headers.origin ?? '*',
|
|
93
|
+
'access-control-allow-credentials': 'true',
|
|
94
|
+
'access-control-allow-headers': '*',
|
|
95
|
+
'access-control-allow-methods': 'GET, POST, OPTIONS',
|
|
96
|
+
};
|
|
97
|
+
const send = (code, body, type = 'text/html', headers = {}) => {
|
|
98
|
+
res.writeHead(code, { 'content-type': `${type}; charset=utf-8`, ...cors, ...headers });
|
|
99
|
+
res.end(body);
|
|
100
|
+
};
|
|
101
|
+
const html = (title, body = '', headers = {}) =>
|
|
102
|
+
send(
|
|
103
|
+
200,
|
|
104
|
+
`<!doctype html><meta charset="utf-8"><title>${title}</title>${body}<img hidden src="/live?tag=${encodeURIComponent(p.get('tag') ?? p.get('n') ?? p.get('case') ?? p.get('value') ?? '')}&to=${encodeURIComponent(url.href)}">`,
|
|
105
|
+
'text/html',
|
|
106
|
+
headers
|
|
107
|
+
);
|
|
108
|
+
route(req, url, send, html);
|
|
109
|
+
};
|
|
110
|
+
const server = tls ? createSecureServer(tls, handler) : createServer(handler);
|
|
111
|
+
await new Promise((resolve, reject) => {
|
|
112
|
+
server.once('error', reject);
|
|
113
|
+
server.listen(port, host, resolve);
|
|
114
|
+
});
|
|
115
|
+
const address = server.address();
|
|
116
|
+
return {
|
|
117
|
+
port: address.port,
|
|
118
|
+
origin: `${scheme}://${host}:${address.port}`,
|
|
119
|
+
log: () => structuredClone(entries),
|
|
120
|
+
reset: () => {
|
|
121
|
+
entries = [];
|
|
122
|
+
},
|
|
123
|
+
close: () =>
|
|
124
|
+
new Promise((resolve, reject) => {
|
|
125
|
+
server.closeAllConnections();
|
|
126
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
127
|
+
}),
|
|
128
|
+
};
|
|
129
|
+
}
|