aztrx-cli 0.2.1 → 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 +1 -0
- package/dist/core/swarm.js +1 -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
|
}
|
|
@@ -117,6 +117,7 @@ export async function run(options) {
|
|
|
117
117
|
baseline,
|
|
118
118
|
guardOn,
|
|
119
119
|
log: say,
|
|
120
|
+
forwardBus: bus,
|
|
120
121
|
});
|
|
121
122
|
// Replays reuse the swarm-captured auth state, or the explicit --storage-state.
|
|
122
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")
|