aztrx-cli 0.2.0 → 0.2.2
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 +4 -4
- package/dist/core/domWalker.js +79 -51
- package/dist/core/orchestrator.js +3 -1
- package/dist/core/swarm.js +1 -1
- package/dist/core/version.js +15 -0
- package/dist/ui/app.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ jobs:
|
|
|
196
196
|
permissions: { contents: read, pull-requests: write }
|
|
197
197
|
steps:
|
|
198
198
|
- uses: actions/checkout@v4
|
|
199
|
-
- uses: DanisChaparov/aztrx@
|
|
199
|
+
- uses: DanisChaparov/aztrx@17366eba81e6a790507d82a96eb4d7fd62f02ccd
|
|
200
200
|
with:
|
|
201
201
|
url: http://localhost:3000
|
|
202
202
|
start-command: npm run dev # optional — boot the app in the background
|
|
@@ -210,7 +210,7 @@ Or as a reusable workflow:
|
|
|
210
210
|
on: pull_request
|
|
211
211
|
jobs:
|
|
212
212
|
aztrx:
|
|
213
|
-
uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@
|
|
213
|
+
uses: DanisChaparov/aztrx/.github/workflows/aztrx-pr.yml@17366eba81e6a790507d82a96eb4d7fd62f02ccd
|
|
214
214
|
with:
|
|
215
215
|
url: http://localhost:3000
|
|
216
216
|
start-command: npm run dev
|
|
@@ -261,7 +261,7 @@ jobs:
|
|
|
261
261
|
sleep 2
|
|
262
262
|
done
|
|
263
263
|
- name: Generate badge
|
|
264
|
-
run: npx --yes aztrx-cli@0.2.
|
|
264
|
+
run: npx --yes aztrx-cli@0.2.2 run http://localhost:3000 --badge badge.svg
|
|
265
265
|
- name: Commit badge
|
|
266
266
|
run: |
|
|
267
267
|
git config user.name "github-actions[bot]"
|
|
@@ -320,7 +320,7 @@ LLM call.
|
|
|
320
320
|
CORS.
|
|
321
321
|
- **`.aztrx/` is gitignored** on `init` — repro specs, reports, and patches stay
|
|
322
322
|
out of history.
|
|
323
|
-
- **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.
|
|
323
|
+
- **Pinned supply chain.** The GitHub Action pins `aztrx-cli@0.2.2` (never
|
|
324
324
|
`@latest`).
|
|
325
325
|
|
|
326
326
|
## Telemetry & privacy
|
package/dist/core/domWalker.js
CHANGED
|
@@ -14,64 +14,92 @@ export async function walkDom(page, bus, opts = {}) {
|
|
|
14
14
|
const max = opts.maxActions ?? 100;
|
|
15
15
|
const startUrl = page.url();
|
|
16
16
|
let actions = 0;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
let tag;
|
|
28
|
-
try {
|
|
29
|
-
tag = await handle.evaluate((el) => el.tagName.toLowerCase());
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
// Re-scan the DOM after every action (the page may have mutated or navigated)
|
|
19
|
+
// rather than walking a stale snapshot. Mirrors the fuzzer's recover-and-iterate
|
|
20
|
+
// loop, but deterministically (document order) and without re-clicking elements
|
|
21
|
+
// it has already acted on.
|
|
22
|
+
while (actions < max) {
|
|
23
|
+
if (page.url() !== startUrl) {
|
|
24
|
+
// A previous action navigated away — return to the starting page.
|
|
25
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
26
|
+
await page.waitForTimeout(300);
|
|
30
27
|
}
|
|
31
|
-
|
|
32
|
-
continue; // element unreadable mid-query — skip
|
|
33
|
-
}
|
|
34
|
-
let label = "";
|
|
28
|
+
let handles;
|
|
35
29
|
try {
|
|
36
|
-
|
|
37
|
-
const t = el.innerText ||
|
|
38
|
-
el.getAttribute("aria-label") ||
|
|
39
|
-
el.getAttribute("value") ||
|
|
40
|
-
el.getAttribute("placeholder") ||
|
|
41
|
-
"";
|
|
42
|
-
return t.trim();
|
|
43
|
-
});
|
|
30
|
+
handles = await page.$$(SELECTOR);
|
|
44
31
|
}
|
|
45
32
|
catch {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
// A navigation raced the re-scan — reset to the start page and retry.
|
|
34
|
+
await page.goto(startUrl, { waitUntil: "domcontentloaded" }).catch(() => { });
|
|
35
|
+
await page.waitForTimeout(300);
|
|
49
36
|
continue;
|
|
50
|
-
if (tag === "a") {
|
|
51
|
-
const href = (await handle.getAttribute("href")) ?? "";
|
|
52
|
-
if (/^https?:\/\//.test(href) && !href.startsWith(originOf(startUrl)))
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if (tag === "input") {
|
|
56
|
-
const type = (await handle.getAttribute("type")) ?? "";
|
|
57
|
-
if (!TEXT_INPUT_TYPES.has(type))
|
|
58
|
-
continue; // skip password/hidden/submit/checkbox/etc.
|
|
59
|
-
}
|
|
60
|
-
const selectors = await selectorCascade(page, handle);
|
|
61
|
-
if (tag === "input" || tag === "textarea") {
|
|
62
|
-
const action = { type: "input", selectors, value: "test", timestamp: Date.now() };
|
|
63
|
-
bus.emit("action", action);
|
|
64
|
-
if (!opts.dryRun)
|
|
65
|
-
await handle.fill("test").catch(() => { });
|
|
66
37
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
38
|
+
let acted = false;
|
|
39
|
+
for (const handle of handles) {
|
|
40
|
+
if (actions >= max)
|
|
41
|
+
break;
|
|
42
|
+
const visible = await handle.isVisible().catch(() => false);
|
|
43
|
+
const enabled = await handle.isEnabled().catch(() => false);
|
|
44
|
+
if (!visible || !enabled)
|
|
45
|
+
continue;
|
|
46
|
+
let tag;
|
|
47
|
+
try {
|
|
48
|
+
tag = await handle.evaluate((el) => el.tagName.toLowerCase());
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue; // element unreadable mid-query — skip
|
|
52
|
+
}
|
|
53
|
+
let label = "";
|
|
54
|
+
try {
|
|
55
|
+
label = await handle.evaluate((el) => {
|
|
56
|
+
const t = el.innerText ||
|
|
57
|
+
el.getAttribute("aria-label") ||
|
|
58
|
+
el.getAttribute("value") ||
|
|
59
|
+
el.getAttribute("placeholder") ||
|
|
60
|
+
"";
|
|
61
|
+
return t.trim();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
label = ""; // degraded — no label to filter on
|
|
66
|
+
}
|
|
67
|
+
if (DESTRUCTIVE.test(label))
|
|
68
|
+
continue;
|
|
69
|
+
if (tag === "a") {
|
|
70
|
+
const href = (await handle.getAttribute("href")) ?? "";
|
|
71
|
+
if (/^https?:\/\//.test(href) && !href.startsWith(originOf(startUrl)))
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (tag === "input") {
|
|
75
|
+
const type = (await handle.getAttribute("type")) ?? "";
|
|
76
|
+
if (!TEXT_INPUT_TYPES.has(type))
|
|
77
|
+
continue; // skip password/hidden/submit/checkbox/etc.
|
|
78
|
+
}
|
|
79
|
+
const selectors = await selectorCascade(page, handle);
|
|
80
|
+
const signature = selectors.join("|") || `${tag}:${label}`;
|
|
81
|
+
if (seen.has(signature))
|
|
82
|
+
continue; // already acted on this element
|
|
83
|
+
seen.add(signature);
|
|
84
|
+
if (tag === "input" || tag === "textarea") {
|
|
85
|
+
const action = { type: "input", selectors, value: "test", timestamp: Date.now() };
|
|
86
|
+
bus.emit("action", action);
|
|
87
|
+
if (!opts.dryRun)
|
|
88
|
+
await handle.fill("test").catch(() => { });
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const action = { type: "click", selectors, timestamp: Date.now() };
|
|
92
|
+
bus.emit("action", action);
|
|
93
|
+
if (!opts.dryRun)
|
|
94
|
+
await handle.click({ timeout: 1500 }).catch(() => { });
|
|
95
|
+
}
|
|
96
|
+
actions++;
|
|
97
|
+
acted = true;
|
|
98
|
+
await page.waitForTimeout(120);
|
|
99
|
+
break; // re-scan next iteration — the DOM may have changed
|
|
72
100
|
}
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
if (!acted)
|
|
102
|
+
break; // nothing left to act on
|
|
75
103
|
}
|
|
76
104
|
return actions;
|
|
77
105
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
2
|
import pc from "picocolors";
|
|
3
|
+
import { VERSION } from "./version.js";
|
|
3
4
|
import { EventBus } from "./eventBus.js";
|
|
4
5
|
import { loadBaseline } from "./classifier.js";
|
|
5
6
|
import { attachNetworkGuard, allowHostsFrom } from "./networkGuard.js";
|
|
@@ -66,7 +67,7 @@ export async function run(options) {
|
|
|
66
67
|
console.log(parts.join(" "));
|
|
67
68
|
};
|
|
68
69
|
const emitPhase = (phase, detail) => bus.emit("phase", { phase, detail, ts: Date.now() });
|
|
69
|
-
say(pc.cyan(
|
|
70
|
+
say(pc.cyan(`\nAztrx AI v${VERSION} — Runtime Detector`));
|
|
70
71
|
say(pc.dim(`Target: ${url}`));
|
|
71
72
|
say(pc.dim(`Repo: ${repoRoot}`));
|
|
72
73
|
if (options.fuzz)
|
|
@@ -116,6 +117,7 @@ export async function run(options) {
|
|
|
116
117
|
baseline,
|
|
117
118
|
guardOn,
|
|
118
119
|
log: say,
|
|
120
|
+
forwardBus: bus,
|
|
119
121
|
});
|
|
120
122
|
// Replays reuse the swarm-captured auth state, or the explicit --storage-state.
|
|
121
123
|
const replayStorageState = swarmAuthState ?? options.storageState;
|
package/dist/core/swarm.js
CHANGED
|
@@ -213,7 +213,7 @@ export async function swarmDetect(opts) {
|
|
|
213
213
|
httpFuzzMutations: opts.httpFuzzMutations,
|
|
214
214
|
baseline: opts.baseline,
|
|
215
215
|
log: (m) => opts.log(strategies.length > 1 ? `[w${i}] ${m}` : m),
|
|
216
|
-
}, strategy)));
|
|
216
|
+
}, strategy, opts.forwardBus)));
|
|
217
217
|
const results = [];
|
|
218
218
|
settled.forEach((r, i) => {
|
|
219
219
|
if (r.status === "fulfilled")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
// Single source of truth for the CLI's self-reported version. Read from the
|
|
4
|
+
// installed package.json so a future `npm version` bump never drifts from the
|
|
5
|
+
// printed banner — a hardcoded "v0.1.1" previously survived the 0.2.0 bump.
|
|
6
|
+
function readVersion() {
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL("../../package.json", import.meta.url);
|
|
9
|
+
return JSON.parse(readFileSync(fileURLToPath(url), "utf-8")).version;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return "0.0.0"; // cosmetic only — package.json not resolvable (raw dist/ checkout)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export const VERSION = readVersion();
|
package/dist/ui/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
|
3
3
|
import { render, Box, Text, useApp } from "ink";
|
|
4
|
+
import { VERSION } from "../core/version.js";
|
|
4
5
|
// Palette — mirrors web/app/globals.css "crash seismograph" tokens, mapped to
|
|
5
6
|
// the nearest ANSI colors so the terminal panel reads as the same instrument.
|
|
6
7
|
const C = {
|
|
@@ -124,7 +125,7 @@ function AztrxApp({ bus, done, targetUrl, repoRoot, mode }) {
|
|
|
124
125
|
}, [done, exit]);
|
|
125
126
|
const phase = PHASE_LABEL[state.phase];
|
|
126
127
|
const currentRoute = state.routes[state.routes.length - 1] ?? targetUrl;
|
|
127
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: C.azure, bold: true, children: "Aztrx AI" }), _jsx(Text, { color: C.dim, children: " \u2014 Runtime Detector" }),
|
|
128
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: C.azure, bold: true, children: "Aztrx AI" }), _jsx(Text, { color: C.dim, children: " \u2014 Runtime Detector" }), _jsxs(Text, { color: C.dim, children: [" v", VERSION] })] }), _jsxs(Text, { color: C.dim, children: [" target ", targetUrl, " repo ", repoRoot] }), _jsxs(Text, { color: C.dim, children: [" mode ", mode] }), _jsx(Text, { color: C.dim, children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: phase.color, children: phase.text }), _jsx(Text, { color: C.dim, children: " " }), _jsx(Text, { color: C.azureBright, bold: true, children: rate.toFixed(1) }), _jsx(Text, { color: C.dim, children: " ops/s \u00B7 " }), _jsx(Text, { color: C.fg, children: state.actions }), _jsx(Text, { color: C.dim, children: " actions \u00B7 " }), _jsx(Text, { color: C.fg, children: state.clicks }), _jsx(Text, { color: C.dim, children: " clicks" })] }), _jsxs(Box, { children: [_jsx(Text, { color: C.dim, children: " route " }), _jsx(Text, { color: C.muted, children: currentRoute }), _jsxs(Text, { color: C.dim, children: [" \u00B7 ", state.routes.length, " route(s)"] })] }), state.findings.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: C.muted, bold: true, children: ["findings (", state.findings.length, ")"] }), state.findings.map((f) => (_jsx(FindingRow, { finding: f, repro: state.repros[f.fingerprint] }, f.fingerprint)))] })) : null, state.noise > 0 ? (_jsxs(Text, { color: C.dim, children: [" \u25B8 ", state.noise, " noise event(s) suppressed"] })) : null] }));
|
|
128
129
|
}
|
|
129
130
|
/** Mount the live terminal panel and resolve once the run (or a failure) ends. */
|
|
130
131
|
export function renderTui(props) {
|