argusqa-os 9.5.5 → 9.6.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 +384 -1100
- package/glama.json +9 -1
- package/package.json +4 -3
- package/src/adapters/browser.js +1 -0
- package/src/cli/init.js +8 -4
- package/src/config/targets.js +10 -0
- package/src/mcp-server.js +115 -2
- package/src/orchestration/dispatcher.js +1 -1
- package/src/orchestration/env-comparison.js +0 -1
- package/src/orchestration/orchestrator.js +10 -5
- package/src/orchestration/report-processor.js +1 -1
- package/src/orchestration/slack-notifier.js +1 -1
- package/src/orchestration/watch-mode.js +0 -4
- package/src/server/index.js +24 -2
- package/src/server/slash-command-handler.js +0 -1
- package/src/utils/a11y-deep-analyzer.js +294 -0
- package/src/utils/baseline-manager.js +3 -3
- package/src/utils/codebase-analyzer.js +3 -3
- package/src/utils/content-analyzer.js +1 -1
- package/src/utils/flow-runner.js +4 -4
- package/src/utils/font-analyzer.js +213 -0
- package/src/utils/form-analyzer.js +247 -0
- package/src/utils/github-reporter.js +221 -18
- package/src/utils/har-recorder.js +197 -0
- package/src/utils/motion-analyzer.js +243 -0
- package/src/utils/pr-diff-analyzer.js +121 -0
- package/src/utils/route-discoverer.js +1 -1
- package/src/utils/security-analyzer.js +1 -1
- package/src/utils/seo-analyzer.js +1 -1
- package/src/utils/session-persistence.js +1 -1
- package/src/utils/visual-diff-analyzer.js +9 -4
package/glama.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://glama.ai/mcp/schemas/server.json",
|
|
3
3
|
"name": "argus",
|
|
4
|
-
"description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations.
|
|
4
|
+
"description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 9 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types), argus_visual_diff (screenshot baseline comparison, updateBaseline flag), argus_pr_validate (PR diff → affected routes → targeted audit → blocked flag). 137 test blocks, 634 hard assertions, 63 detection categories.",
|
|
5
5
|
"maintainers": ["ironclawdevs27"],
|
|
6
6
|
"tools": [
|
|
7
7
|
{
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
{
|
|
32
32
|
"name": "argus_design_audit",
|
|
33
33
|
"description": "Full Figma design-to-implementation fidelity audit. Fetches design spec from a Figma frame URL (requires FIGMA_API_TOKEN) and compares every extracted property against live DOM computed styles. Detects 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB distance), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y vs Figma bounds), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node."
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "argus_visual_diff",
|
|
37
|
+
"description": "Screenshot baseline comparison for a URL. First call saves a baseline PNG to reports/baselines/screenshots/. Subsequent calls diff the current screenshot against the baseline using pixelmatch and return visual_regression (warning ≥0.1% / critical ≥5% pixels changed) + visual_diff_summary (always). Pass updateBaseline: true to force-refresh the stored baseline after intentional UI changes."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "argus_pr_validate",
|
|
41
|
+
"description": "Targeted QA audit driven by a GitHub pull request diff. Fetches the PR's changed files, maps them to affected routes in your target config using path-slug heuristics (infrastructure changes trigger a full audit), then audits only those routes. Returns { findings, affectedRoutes, changedFiles, perRoute, summary, blocked, blockOn }. Use in CI to gate merges — blocked:true when findings meet the blockOn threshold (none/warning/critical, default: critical). Requires Chrome on --remote-debugging-port=9222. GITHUB_TOKEN env var recommended for private repos."
|
|
34
42
|
}
|
|
35
43
|
]
|
|
36
44
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "argusqa-os",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.6.0",
|
|
4
4
|
"mcpName": "io.github.ironclawdevs27/argus",
|
|
5
5
|
"description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
|
|
6
6
|
"keywords": [
|
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
"@opentelemetry/api": "^1.9.1",
|
|
56
56
|
"@opentelemetry/sdk-node": "^0.218.0",
|
|
57
57
|
"@slack/web-api": "^7.16.0",
|
|
58
|
-
"
|
|
58
|
+
"axe-core": "^4.12.0",
|
|
59
|
+
"dotenv": "^17.4.2",
|
|
59
60
|
"express": "^5.2.1",
|
|
60
61
|
"pino": "^10.3.1",
|
|
61
62
|
"pino-pretty": "^13.1.3",
|
|
@@ -64,6 +65,6 @@
|
|
|
64
65
|
"zod": "^4.4.3"
|
|
65
66
|
},
|
|
66
67
|
"devDependencies": {
|
|
67
|
-
"vitest": "^4.1.
|
|
68
|
+
"vitest": "^4.1.8"
|
|
68
69
|
}
|
|
69
70
|
}
|
package/src/adapters/browser.js
CHANGED
|
@@ -48,6 +48,7 @@ export class CdpBrowserAdapter {
|
|
|
48
48
|
emulate(viewport) { return this._mcp.emulate({ viewport }); }
|
|
49
49
|
emulateCpu(rate) { return this._mcp.emulate({ cpuThrottlingRate: rate }); }
|
|
50
50
|
emulateColorScheme(scheme) { return this._mcp.emulate({ colorScheme: scheme }); }
|
|
51
|
+
emulateReducedMotion(pref) { return this._mcp.emulate({ reducedMotion: pref }); }
|
|
51
52
|
resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
|
|
52
53
|
|
|
53
54
|
// ── Network & performance ───────────────────────────────────────────────────
|
package/src/cli/init.js
CHANGED
|
@@ -287,11 +287,15 @@ async function main() {
|
|
|
287
287
|
githubToken, githubRepo, sourceDir, envFile: envFilePath });
|
|
288
288
|
const targetsContent = generateTargetsJs(finalRoutes, { framework, sourceDir, envFile: envFilePath });
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
} else {
|
|
293
|
-
fs.writeFileSync('.env', envContent, 'utf8');
|
|
290
|
+
try {
|
|
291
|
+
fs.writeFileSync('.env', envContent, { flag: 'wx', encoding: 'utf8' });
|
|
294
292
|
tick('Wrote .env');
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (err.code === 'EEXIST') {
|
|
295
|
+
logger.warn(' ⚠ .env already exists — skipping write to preserve existing credentials. Delete it manually to regenerate.');
|
|
296
|
+
} else {
|
|
297
|
+
throw err;
|
|
298
|
+
}
|
|
295
299
|
}
|
|
296
300
|
|
|
297
301
|
const targetsPath = path.join('src', 'config', 'targets.js');
|
package/src/config/targets.js
CHANGED
|
@@ -68,6 +68,16 @@ export const thresholds = {
|
|
|
68
68
|
warnPercent: parseFloat(process.env.VISUAL_WARN_PERCENT ?? '0.1'), // % pixels changed → warning
|
|
69
69
|
critPercent: parseFloat(process.env.VISUAL_CRIT_PERCENT ?? '5.0'), // % pixels changed → critical
|
|
70
70
|
},
|
|
71
|
+
a11y: {
|
|
72
|
+
contrastAA: parseFloat(process.env.A11Y_CONTRAST_AA ?? '4.5'), // WCAG AA normal text contrast ratio
|
|
73
|
+
maxAxeViolations: parseInt(process.env.A11Y_MAX_AXE ?? '50', 10), // cap axe-core violations per run
|
|
74
|
+
},
|
|
75
|
+
motion: {
|
|
76
|
+
animationPropertyCount: parseInt(process.env.MOTION_ANIM_COUNT ?? '1', 10), // flag interactive animations at this count
|
|
77
|
+
},
|
|
78
|
+
font: {
|
|
79
|
+
slowLoadMs: parseInt(process.env.FONT_SLOW_MS ?? '1000', 10), // ms threshold for slow font load warning
|
|
80
|
+
},
|
|
71
81
|
};
|
|
72
82
|
|
|
73
83
|
/**
|
package/src/mcp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Argus MCP Server (v9.
|
|
3
|
+
* Argus MCP Server (v9.6.0)
|
|
4
4
|
*
|
|
5
5
|
* Exposes Argus as an MCP server so Claude (or any MCP client) can call
|
|
6
6
|
* argus_audit, argus_audit_full, argus_compare, argus_last_report, and
|
|
@@ -32,6 +32,8 @@ import { WatchSession } from './orchestration/watch-mode.j
|
|
|
32
32
|
import { CdpBrowserAdapter } from './adapters/browser.js';
|
|
33
33
|
import { getFigmaFrame } from './adapters/figma.js';
|
|
34
34
|
import { analyzeDesignFidelity } from './utils/design-fidelity-analyzer.js';
|
|
35
|
+
import { analyzeVisualRegression } from './utils/visual-diff-analyzer.js';
|
|
36
|
+
import { parsePrUrl, fetchPrFiles, mapFilesToRoutes } from './utils/pr-diff-analyzer.js';
|
|
35
37
|
|
|
36
38
|
const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
|
|
37
39
|
|
|
@@ -119,6 +121,19 @@ const TOOLS = [
|
|
|
119
121
|
},
|
|
120
122
|
},
|
|
121
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: 'argus_visual_diff',
|
|
126
|
+
description: 'Screenshot baseline comparison for a URL — captures a PNG screenshot and compares it pixel-by-pixel against a stored baseline using pixelmatch. First call: saves baseline, returns visual_baseline_created (info). Subsequent calls: returns visual_regression (warning ≥0.1% / critical ≥5% pixels changed) + visual_diff_summary (always). Baseline stored in reports/baselines/screenshots/. Use in CI or fix loops to detect unintended visual regressions without a full audit. Pass updateBaseline: true to force-refresh the stored baseline (e.g. after intentional UI changes). Requires Chrome on --remote-debugging-port=9222.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
url: { type: 'string', description: 'Full URL to capture and compare (e.g. http://localhost:3000/dashboard). Must be reachable by the running Chrome instance.' },
|
|
131
|
+
updateBaseline: { type: 'boolean', description: 'When true, deletes the existing baseline PNG and saves a fresh one from the current screenshot. Use after intentional UI changes to reset the reference.', default: false },
|
|
132
|
+
baselineDir: { type: 'string', description: 'Optional override for the baseline storage directory. Defaults to reports/baselines/screenshots/.' },
|
|
133
|
+
},
|
|
134
|
+
required: ['url'],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
122
137
|
{
|
|
123
138
|
name: 'argus_design_audit',
|
|
124
139
|
description: 'Full design-to-implementation fidelity audit against a Figma frame. 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB delta), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y, 20px threshold), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node. Requires FIGMA_API_TOKEN env var and Chrome on --remote-debugging-port=9222. Returns { findings, summary } where summary includes 13 mismatch-type counts.',
|
|
@@ -131,6 +146,20 @@ const TOOLS = [
|
|
|
131
146
|
required: ['url', 'figmaFrameUrl'],
|
|
132
147
|
},
|
|
133
148
|
},
|
|
149
|
+
{
|
|
150
|
+
name: 'argus_pr_validate',
|
|
151
|
+
description: 'Runs a targeted Argus audit on the routes affected by a GitHub pull request. Fetches the PR diff, maps changed files to routes in your target config using path-slug heuristics (infrastructure changes trigger a full audit; targeted otherwise), and audits only those routes — faster than a full scan and focused on what the PR actually touched. Returns { findings, affectedRoutes, changedFiles, perRoute, summary, blocked, blockOn }. Use in CI to gate merges: check blocked:true or pipe findings to an AI verdict step. Requires Chrome on --remote-debugging-port=9222. GITHUB_TOKEN env var recommended for private repos.',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
prUrl: { type: 'string', description: 'Full GitHub PR URL (e.g. https://github.com/owner/repo/pull/42). Used to fetch the list of changed files via the GitHub REST API.' },
|
|
156
|
+
targetUrl: { type: 'string', description: 'Base URL to audit (e.g. https://staging.example.com). Overrides TARGET_DEV_URL env var.' },
|
|
157
|
+
githubToken: { type: 'string', description: 'GitHub Personal Access Token or workflow GITHUB_TOKEN. Optional for public repos. Falls back to GITHUB_TOKEN env var.' },
|
|
158
|
+
blockOn: { type: 'string', enum: ['none', 'warning', 'critical'], description: '"critical" = block only when critical findings exist. "warning" = block on any warning or critical. "none" = never block. Defaults to ARGUS_BLOCK_ON env var, then "critical".', default: 'critical' },
|
|
159
|
+
},
|
|
160
|
+
required: ['prUrl'],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
134
163
|
];
|
|
135
164
|
|
|
136
165
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -282,6 +311,42 @@ async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
|
|
|
282
311
|
});
|
|
283
312
|
}
|
|
284
313
|
|
|
314
|
+
async function handleVisualDiff({ url, updateBaseline = false, baselineDir }) {
|
|
315
|
+
if (!url) throw new Error('argus_visual_diff: url is required');
|
|
316
|
+
|
|
317
|
+
return withMcp(async (mcp) => {
|
|
318
|
+
const browser = new CdpBrowserAdapter(mcp);
|
|
319
|
+
const opts = baselineDir ? { baselineDir } : {};
|
|
320
|
+
|
|
321
|
+
if (updateBaseline) {
|
|
322
|
+
// Delete existing baseline so analyzeVisualRegression treats it as first run
|
|
323
|
+
const path_ = await import('path');
|
|
324
|
+
const fs_ = await import('fs');
|
|
325
|
+
const { slugify } = await import('./utils/slug.js');
|
|
326
|
+
const { config } = await import('./config/targets.js');
|
|
327
|
+
const dir = baselineDir ?? path_.default.join(config.outputDir, 'baselines', 'screenshots');
|
|
328
|
+
const file = path_.default.join(dir, `${slugify(url)}.png`);
|
|
329
|
+
try { fs_.default.unlinkSync(file); } catch {}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const findings = await analyzeVisualRegression(browser, url, opts);
|
|
333
|
+
const regression = findings.find(f => f.type === 'visual_regression');
|
|
334
|
+
const baseline = findings.find(f => f.type === 'visual_baseline_created');
|
|
335
|
+
const summary = findings.find(f => f.type === 'visual_diff_summary');
|
|
336
|
+
|
|
337
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
338
|
+
findings,
|
|
339
|
+
summary: {
|
|
340
|
+
status: regression ? 'regression' : baseline ? 'baseline_created' : 'no_change',
|
|
341
|
+
diffPercent: summary?.diffPercent ?? 0,
|
|
342
|
+
diffPixels: summary?.diffPixels ?? 0,
|
|
343
|
+
totalPixels: summary?.totalPixels ?? 0,
|
|
344
|
+
severity: regression?.severity ?? 'info',
|
|
345
|
+
},
|
|
346
|
+
}, null, 2) }] };
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
285
350
|
async function handleDesignAudit({ url, figmaFrameUrl }) {
|
|
286
351
|
if (!url) throw new Error('argus_design_audit: url is required');
|
|
287
352
|
if (!figmaFrameUrl) throw new Error('argus_design_audit: figmaFrameUrl is required');
|
|
@@ -318,6 +383,52 @@ async function handleDesignAudit({ url, figmaFrameUrl }) {
|
|
|
318
383
|
});
|
|
319
384
|
}
|
|
320
385
|
|
|
386
|
+
async function handlePrValidate({ prUrl, targetUrl, githubToken, blockOn } = {}) {
|
|
387
|
+
if (!prUrl) throw new Error('argus_pr_validate: prUrl is required');
|
|
388
|
+
|
|
389
|
+
const { routes } = await import('./config/targets.js');
|
|
390
|
+
const token = githubToken ?? process.env.GITHUB_TOKEN;
|
|
391
|
+
const base = targetUrl ?? process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
392
|
+
const policy = blockOn ?? process.env.ARGUS_BLOCK_ON ?? 'critical';
|
|
393
|
+
|
|
394
|
+
const changedFiles = await fetchPrFiles(prUrl, token);
|
|
395
|
+
const affectedRoutes = mapFilesToRoutes(changedFiles, routes ?? []);
|
|
396
|
+
|
|
397
|
+
const allFindings = [];
|
|
398
|
+
const perRoute = [];
|
|
399
|
+
|
|
400
|
+
for (const route of affectedRoutes) {
|
|
401
|
+
const url = new URL(route.path, base).href;
|
|
402
|
+
const res = await handleAudit({ url, critical: route.critical ?? false });
|
|
403
|
+
const data = JSON.parse(res.content[0].text);
|
|
404
|
+
allFindings.push(...(data.findings ?? []));
|
|
405
|
+
perRoute.push({ route: route.path, ...data.summary });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const summary = {
|
|
409
|
+
critical: allFindings.filter(f => f.severity === 'critical').length,
|
|
410
|
+
warning: allFindings.filter(f => f.severity === 'warning').length,
|
|
411
|
+
info: allFindings.filter(f => f.severity === 'info').length,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const blocked =
|
|
415
|
+
policy === 'critical' ? summary.critical > 0 :
|
|
416
|
+
policy === 'warning' ? summary.critical + summary.warning > 0 :
|
|
417
|
+
false;
|
|
418
|
+
|
|
419
|
+
return { content: [{ type: 'text', text: JSON.stringify({
|
|
420
|
+
prUrl,
|
|
421
|
+
targetUrl: base,
|
|
422
|
+
affectedRoutes: affectedRoutes.map(r => r.path),
|
|
423
|
+
changedFiles,
|
|
424
|
+
findings: allFindings,
|
|
425
|
+
perRoute,
|
|
426
|
+
summary,
|
|
427
|
+
blocked,
|
|
428
|
+
blockOn: policy,
|
|
429
|
+
}, null, 2) }] };
|
|
430
|
+
}
|
|
431
|
+
|
|
321
432
|
async function handleLastReport() {
|
|
322
433
|
if (!fs.existsSync(REPORTS_DIR)) {
|
|
323
434
|
return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
|
|
@@ -336,7 +447,7 @@ async function handleLastReport() {
|
|
|
336
447
|
// ── Server bootstrap ──────────────────────────────────────────────────────────
|
|
337
448
|
|
|
338
449
|
const server = new Server(
|
|
339
|
-
{ name: 'argus', version: '9.
|
|
450
|
+
{ name: 'argus', version: '9.6.0' },
|
|
340
451
|
{ capabilities: { tools: {} } },
|
|
341
452
|
);
|
|
342
453
|
|
|
@@ -351,7 +462,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
351
462
|
case 'argus_last_report': return await handleLastReport();
|
|
352
463
|
case 'argus_watch_snapshot': return await handleWatchSnapshot(req.params.arguments ?? {});
|
|
353
464
|
case 'argus_get_context': return await handleGetContext(req.params.arguments ?? {});
|
|
465
|
+
case 'argus_visual_diff': return await handleVisualDiff(req.params.arguments ?? {});
|
|
354
466
|
case 'argus_design_audit': return await handleDesignAudit(req.params.arguments ?? {});
|
|
467
|
+
case 'argus_pr_validate': return await handlePrValidate(req.params.arguments ?? {});
|
|
355
468
|
default: throw new Error(`Unknown tool: ${req.params.name}`);
|
|
356
469
|
}
|
|
357
470
|
} catch (err) {
|
|
@@ -26,7 +26,7 @@ function openInBrowser(filePath) {
|
|
|
26
26
|
try {
|
|
27
27
|
const abs = path.resolve(filePath);
|
|
28
28
|
if (process.platform === 'win32') {
|
|
29
|
-
execFile('cmd', ['/c', 'start', '', abs], () => {});
|
|
29
|
+
execFile('cmd', ['/c', 'start', '', abs], () => {}); // lgtm[js/shell-command-injection-from-environment] — execFile with args array is injection-safe; abs is path.resolve() of an internally-computed report path
|
|
30
30
|
} else if (process.platform === 'darwin') {
|
|
31
31
|
execFile('open', [abs], () => {});
|
|
32
32
|
} else {
|
|
@@ -21,7 +21,6 @@ import { unwrapEval } from '../utils/mcp-client.js';
|
|
|
21
21
|
import { childLogger } from '../utils/logger.js';
|
|
22
22
|
|
|
23
23
|
const logger = childLogger('env-comparison');
|
|
24
|
-
import { normalizeArray } from '../utils/flow-runner.js';
|
|
25
24
|
import { CdpBrowserAdapter } from '../adapters/browser.js';
|
|
26
25
|
|
|
27
26
|
import { comparisonRoutes, config } from '../config/targets.js';
|
|
@@ -45,6 +45,11 @@ import '../utils/theme-analyzer.js';
|
|
|
45
45
|
import '../utils/design-fidelity-analyzer.js';
|
|
46
46
|
import '../utils/web-vitals-analyzer.js';
|
|
47
47
|
import '../utils/visual-diff-analyzer.js';
|
|
48
|
+
import '../utils/a11y-deep-analyzer.js';
|
|
49
|
+
import '../utils/har-recorder.js';
|
|
50
|
+
import '../utils/motion-analyzer.js';
|
|
51
|
+
import '../utils/font-analyzer.js';
|
|
52
|
+
import '../utils/form-analyzer.js';
|
|
48
53
|
|
|
49
54
|
import { getExpensive } from '../registry.js';
|
|
50
55
|
import { deduplicateFindings as deduplicateErrors } from './report-processor.js';
|
|
@@ -485,7 +490,7 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
|
|
|
485
490
|
const text = (msg.text ?? msg.message ?? '');
|
|
486
491
|
if (text.toLowerCase().includes('has been blocked by cors policy')) continue;
|
|
487
492
|
const severity = classifyConsoleMessage(msg, route.critical);
|
|
488
|
-
if (
|
|
493
|
+
if (msg.level !== 'log') {
|
|
489
494
|
result.errors.push({
|
|
490
495
|
type: 'console',
|
|
491
496
|
level: msg.level,
|
|
@@ -1021,10 +1026,10 @@ export async function runCrawl(mcp, routeOverrides = null, baseUrlOverride = nul
|
|
|
1021
1026
|
// Auth session persistence (B2)
|
|
1022
1027
|
const sessionFile = auth?.sessionFile ?? '.argus-session.json';
|
|
1023
1028
|
if (auth?.steps?.length > 0) {
|
|
1024
|
-
if (!hasSession(sessionFile, auth
|
|
1025
|
-
logger.info(`[ARGUS] Auth: running login flow (${auth
|
|
1029
|
+
if (!hasSession(sessionFile, auth?.sessionMaxAgeMs)) {
|
|
1030
|
+
logger.info(`[ARGUS] Auth: running login flow (${auth?.steps?.length ?? 0} steps)...`);
|
|
1026
1031
|
try {
|
|
1027
|
-
await runLoginFlow(browser, targetBaseUrl, auth
|
|
1032
|
+
await runLoginFlow(browser, targetBaseUrl, auth?.steps ?? []);
|
|
1028
1033
|
await saveSession(browser, sessionFile);
|
|
1029
1034
|
} catch (err) {
|
|
1030
1035
|
logger.warn(`[ARGUS] Auth: login flow failed — crawl will proceed unauthenticated: ${err.message}`);
|
|
@@ -1149,5 +1154,5 @@ export async function runCrawl(mcp, routeOverrides = null, baseUrlOverride = nul
|
|
|
1149
1154
|
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
1150
1155
|
logger.info('[ARGUS] orchestrator.js loaded. Invoke runCrawl(mcp) from Claude Code with MCP tools connected.');
|
|
1151
1156
|
logger.info('[ARGUS] Target base URL: ' + BASE_URL);
|
|
1152
|
-
logger.info('[ARGUS] Routes to crawl: ' +
|
|
1157
|
+
logger.info('[ARGUS] Routes to crawl: ' + routes.map(r => r?.path ?? '(no path)').join(', '));
|
|
1153
1158
|
}
|
|
@@ -108,7 +108,7 @@ export async function processReport(report, { outputDir, severityOverrides }) {
|
|
|
108
108
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
109
109
|
const reportPath = path.join(outputDir, `error-report-${timestamp}.json`);
|
|
110
110
|
try {
|
|
111
|
-
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
111
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); // lgtm[js/network-data-to-file] — intentional: Argus persists crawl findings to a local JSON report file by design
|
|
112
112
|
} catch (err) {
|
|
113
113
|
logger.error(`[ARGUS] Failed to write report JSON: ${err.message}`);
|
|
114
114
|
throw err;
|
|
@@ -99,7 +99,7 @@ async function uploadFileToSlack(filePath, channelId, filename) {
|
|
|
99
99
|
const response = await fetch(uploadUrl, {
|
|
100
100
|
method: 'PUT',
|
|
101
101
|
headers: { 'Content-Type': 'application/octet-stream' },
|
|
102
|
-
body: fileBuffer,
|
|
102
|
+
body: fileBuffer, // lgtm[js/file-access-to-http] — intentional screenshot upload to Slack pre-signed URL; file path is internally generated by Argus, not from HTTP request input
|
|
103
103
|
signal: AbortSignal.timeout(30000),
|
|
104
104
|
});
|
|
105
105
|
if (!response.ok) {
|
|
@@ -35,10 +35,6 @@ import {
|
|
|
35
35
|
import { postBugReport } from './slack-notifier.js';
|
|
36
36
|
import { isSlackConfigured } from '../utils/slack-guard.js';
|
|
37
37
|
import { generateHtmlReport } from '../utils/html-reporter.js';
|
|
38
|
-
import {
|
|
39
|
-
parseConsoleMsgResponse,
|
|
40
|
-
parseNetworkReqResponse,
|
|
41
|
-
} from '../utils/mcp-parsers.js';
|
|
42
38
|
|
|
43
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
44
40
|
const REPORTS_DIR = path.resolve(__dirname, '../../reports');
|
package/src/server/index.js
CHANGED
|
@@ -50,6 +50,28 @@ app.use((req, res, next) => {
|
|
|
50
50
|
next();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
// ── Rate limiting (per-IP, in-memory) ──────────────────────────────────────────
|
|
54
|
+
// 30 requests per minute per IP — prevents Slack endpoint flooding.
|
|
55
|
+
// Slack signature verification rejects invalid payloads, but rate limiting adds
|
|
56
|
+
// a defence-in-depth layer before signature verification even runs.
|
|
57
|
+
const RATE_WINDOW_MS = 60_000;
|
|
58
|
+
const RATE_MAX = 30;
|
|
59
|
+
const rateLimitMap = new Map();
|
|
60
|
+
|
|
61
|
+
function slackRateLimit(req, res, next) {
|
|
62
|
+
const key = req.ip ?? 'unknown';
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const entry = rateLimitMap.get(key) ?? { count: 0, start: now };
|
|
65
|
+
if (now - entry.start > RATE_WINDOW_MS) { entry.count = 0; entry.start = now; }
|
|
66
|
+
entry.count++;
|
|
67
|
+
rateLimitMap.set(key, entry);
|
|
68
|
+
if (entry.count > RATE_MAX) {
|
|
69
|
+
res.setHeader('Retry-After', Math.ceil(RATE_WINDOW_MS / 1000));
|
|
70
|
+
return res.status(429).json({ error: 'Too Many Requests' });
|
|
71
|
+
}
|
|
72
|
+
next();
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
// ── Routes ─────────────────────────────────────────────────────────────────────
|
|
54
76
|
|
|
55
77
|
app.get('/health', (req, res) => {
|
|
@@ -57,10 +79,10 @@ app.get('/health', (req, res) => {
|
|
|
57
79
|
});
|
|
58
80
|
|
|
59
81
|
// Slack slash commands
|
|
60
|
-
app.post('/slack/commands', handleSlashCommand);
|
|
82
|
+
app.post('/slack/commands', slackRateLimit, handleSlashCommand);
|
|
61
83
|
|
|
62
84
|
// Slack Block Kit interactions (button clicks)
|
|
63
|
-
app.post('/slack/interactions', handleInteraction);
|
|
85
|
+
app.post('/slack/interactions', slackRateLimit, handleInteraction);
|
|
64
86
|
|
|
65
87
|
// ── Start ──────────────────────────────────────────────────────────────────────
|
|
66
88
|
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import crypto from 'crypto';
|
|
18
|
-
import { postBugReport } from '../orchestration/slack-notifier.js';
|
|
19
18
|
import { createMcpClient } from '../utils/mcp-client.js';
|
|
20
19
|
import { runCrawl } from '../orchestration/crawl-and-report.js';
|
|
21
20
|
import { WebClient } from '@slack/web-api';
|