@avi770/testteam 1.2.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/agents/01-analyst.ts +100 -0
- package/agents/02-seed-architect.ts +59 -0
- package/agents/03-test-generator.ts +191 -0
- package/agents/04-unit-runner.ts +160 -0
- package/agents/05-browser-crawler.ts +790 -0
- package/agents/06-api-exerciser.ts +311 -0
- package/agents/07-security-scout.ts +188 -0
- package/agents/08-a11y-guardian.ts +212 -0
- package/agents/09-healer.ts +228 -0
- package/agents/10-reporter.ts +266 -0
- package/agents/11-fixer.ts +253 -0
- package/agents/12-ux-inspector.ts +444 -0
- package/agents/13-performance-profiler.ts +271 -0
- package/agents/14-data-integrity-auditor.ts +417 -0
- package/agents/15-regression-sentinel.ts +307 -0
- package/agents/16-chaos-agent.ts +228 -0
- package/agents/17-documentation-validator.ts +266 -0
- package/agents/18-integration-watchdog.ts +178 -0
- package/agents/19-tenant-isolation-auditor.ts +199 -0
- package/agents/20-workflow-completion-tester.ts +203 -0
- package/agents/21-state-session-tester.ts +262 -0
- package/agents/22-email-notification-verifier.ts +244 -0
- package/agents/23-migration-tester.ts +80 -0
- package/agents/__tests__/01-analyst.test.ts +188 -0
- package/agents/__tests__/02-seed-architect.test.ts +152 -0
- package/agents/__tests__/03-test-generator-full.test.ts +321 -0
- package/agents/__tests__/03-test-generator.test.ts +318 -0
- package/agents/__tests__/04-unit-runner.test.ts +320 -0
- package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
- package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
- package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
- package/agents/__tests__/05-browser-crawler.test.ts +518 -0
- package/agents/__tests__/06-api-exerciser.test.ts +619 -0
- package/agents/__tests__/07-security-scout.test.ts +382 -0
- package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
- package/agents/__tests__/09-healer.test.ts +384 -0
- package/agents/__tests__/10-reporter.test.ts +366 -0
- package/agents/__tests__/11-fixer.test.ts +406 -0
- package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
- package/agents/__tests__/12-ux-inspector.test.ts +443 -0
- package/agents/__tests__/13-performance-profiler.test.ts +411 -0
- package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
- package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
- package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
- package/agents/__tests__/16-chaos-agent.test.ts +427 -0
- package/agents/__tests__/17-documentation-validator.test.ts +402 -0
- package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
- package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
- package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
- package/agents/__tests__/21-state-session-tester.test.ts +374 -0
- package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
- package/agents/__tests__/23-migration-tester.test.ts +145 -0
- package/agents/__tests__/base-agent.test.ts +188 -0
- package/agents/__tests__/registry.test.ts +218 -0
- package/agents/base-agent.ts +77 -0
- package/agents/registry.ts +136 -0
- package/baselines/api-schemas/.gitkeep +0 -0
- package/baselines/performance/.gitkeep +0 -0
- package/baselines/screenshots/.gitkeep +0 -0
- package/bin/testteam.js +10 -0
- package/core/__tests__/ci-output.test.ts +430 -0
- package/core/__tests__/cli.test.ts +387 -0
- package/core/__tests__/config.test.ts +78 -0
- package/core/__tests__/cost-tracker.test.ts +158 -0
- package/core/__tests__/evidence.test.ts +265 -0
- package/core/__tests__/fix-loop.test.ts +210 -0
- package/core/__tests__/health-check.test.ts +44 -0
- package/core/__tests__/init.test.ts +609 -0
- package/core/__tests__/integration.test.ts +204 -0
- package/core/__tests__/license-gen.test.ts +227 -0
- package/core/__tests__/license.test.ts +326 -0
- package/core/__tests__/multi-browser.test.ts +278 -0
- package/core/__tests__/orchestrator.test.ts +519 -0
- package/core/__tests__/phase-gate.test.ts +43 -0
- package/core/__tests__/report-html.test.ts +398 -0
- package/core/__tests__/report-upload.test.ts +325 -0
- package/core/__tests__/run-counter.test.ts +234 -0
- package/core/ci-output.ts +240 -0
- package/core/cli.ts +232 -0
- package/core/config.ts +178 -0
- package/core/cost-tracker.ts +59 -0
- package/core/evidence.ts +132 -0
- package/core/fix-loop.ts +85 -0
- package/core/health-check.ts +54 -0
- package/core/init.ts +546 -0
- package/core/license-gen.ts +212 -0
- package/core/license.ts +211 -0
- package/core/messages.ts +67 -0
- package/core/multi-browser.ts +136 -0
- package/core/orchestrator.ts +354 -0
- package/core/phase-gate.ts +55 -0
- package/core/report-html.ts +657 -0
- package/core/report-upload.ts +188 -0
- package/core/run-counter.ts +175 -0
- package/core/types.ts +57 -0
- package/dist/agents/01-analyst.d.ts +11 -0
- package/dist/agents/01-analyst.d.ts.map +1 -0
- package/dist/agents/01-analyst.js +75 -0
- package/dist/agents/01-analyst.js.map +1 -0
- package/dist/agents/02-seed-architect.d.ts +11 -0
- package/dist/agents/02-seed-architect.d.ts.map +1 -0
- package/dist/agents/02-seed-architect.js +51 -0
- package/dist/agents/02-seed-architect.js.map +1 -0
- package/dist/agents/03-test-generator.d.ts +9 -0
- package/dist/agents/03-test-generator.d.ts.map +1 -0
- package/dist/agents/03-test-generator.js +167 -0
- package/dist/agents/03-test-generator.js.map +1 -0
- package/dist/agents/04-unit-runner.d.ts +9 -0
- package/dist/agents/04-unit-runner.d.ts.map +1 -0
- package/dist/agents/04-unit-runner.js +113 -0
- package/dist/agents/04-unit-runner.js.map +1 -0
- package/dist/agents/05-browser-crawler.d.ts +30 -0
- package/dist/agents/05-browser-crawler.d.ts.map +1 -0
- package/dist/agents/05-browser-crawler.js +685 -0
- package/dist/agents/05-browser-crawler.js.map +1 -0
- package/dist/agents/06-api-exerciser.d.ts +23 -0
- package/dist/agents/06-api-exerciser.d.ts.map +1 -0
- package/dist/agents/06-api-exerciser.js +253 -0
- package/dist/agents/06-api-exerciser.js.map +1 -0
- package/dist/agents/07-security-scout.d.ts +11 -0
- package/dist/agents/07-security-scout.d.ts.map +1 -0
- package/dist/agents/07-security-scout.js +142 -0
- package/dist/agents/07-security-scout.js.map +1 -0
- package/dist/agents/08-a11y-guardian.d.ts +13 -0
- package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
- package/dist/agents/08-a11y-guardian.js +176 -0
- package/dist/agents/08-a11y-guardian.js.map +1 -0
- package/dist/agents/09-healer.d.ts +33 -0
- package/dist/agents/09-healer.d.ts.map +1 -0
- package/dist/agents/09-healer.js +167 -0
- package/dist/agents/09-healer.js.map +1 -0
- package/dist/agents/10-reporter.d.ts +26 -0
- package/dist/agents/10-reporter.d.ts.map +1 -0
- package/dist/agents/10-reporter.js +215 -0
- package/dist/agents/10-reporter.js.map +1 -0
- package/dist/agents/11-fixer.d.ts +26 -0
- package/dist/agents/11-fixer.d.ts.map +1 -0
- package/dist/agents/11-fixer.js +195 -0
- package/dist/agents/11-fixer.js.map +1 -0
- package/dist/agents/12-ux-inspector.d.ts +15 -0
- package/dist/agents/12-ux-inspector.d.ts.map +1 -0
- package/dist/agents/12-ux-inspector.js +364 -0
- package/dist/agents/12-ux-inspector.js.map +1 -0
- package/dist/agents/13-performance-profiler.d.ts +13 -0
- package/dist/agents/13-performance-profiler.d.ts.map +1 -0
- package/dist/agents/13-performance-profiler.js +216 -0
- package/dist/agents/13-performance-profiler.js.map +1 -0
- package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
- package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
- package/dist/agents/14-data-integrity-auditor.js +356 -0
- package/dist/agents/14-data-integrity-auditor.js.map +1 -0
- package/dist/agents/15-regression-sentinel.d.ts +25 -0
- package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
- package/dist/agents/15-regression-sentinel.js +251 -0
- package/dist/agents/15-regression-sentinel.js.map +1 -0
- package/dist/agents/16-chaos-agent.d.ts +9 -0
- package/dist/agents/16-chaos-agent.d.ts.map +1 -0
- package/dist/agents/16-chaos-agent.js +207 -0
- package/dist/agents/16-chaos-agent.js.map +1 -0
- package/dist/agents/17-documentation-validator.d.ts +31 -0
- package/dist/agents/17-documentation-validator.d.ts.map +1 -0
- package/dist/agents/17-documentation-validator.js +246 -0
- package/dist/agents/17-documentation-validator.js.map +1 -0
- package/dist/agents/18-integration-watchdog.d.ts +10 -0
- package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
- package/dist/agents/18-integration-watchdog.js +138 -0
- package/dist/agents/18-integration-watchdog.js.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.js +166 -0
- package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
- package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
- package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
- package/dist/agents/20-workflow-completion-tester.js +159 -0
- package/dist/agents/20-workflow-completion-tester.js.map +1 -0
- package/dist/agents/21-state-session-tester.d.ts +10 -0
- package/dist/agents/21-state-session-tester.d.ts.map +1 -0
- package/dist/agents/21-state-session-tester.js +233 -0
- package/dist/agents/21-state-session-tester.js.map +1 -0
- package/dist/agents/22-email-notification-verifier.d.ts +11 -0
- package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
- package/dist/agents/22-email-notification-verifier.js +199 -0
- package/dist/agents/22-email-notification-verifier.js.map +1 -0
- package/dist/agents/23-migration-tester.d.ts +10 -0
- package/dist/agents/23-migration-tester.d.ts.map +1 -0
- package/dist/agents/23-migration-tester.js +74 -0
- package/dist/agents/23-migration-tester.js.map +1 -0
- package/dist/agents/base-agent.d.ts +19 -0
- package/dist/agents/base-agent.d.ts.map +1 -0
- package/dist/agents/base-agent.js +67 -0
- package/dist/agents/base-agent.js.map +1 -0
- package/dist/agents/registry.d.ts +29 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +117 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/core/ci-output.d.ts +35 -0
- package/dist/core/ci-output.d.ts.map +1 -0
- package/dist/core/ci-output.js +193 -0
- package/dist/core/ci-output.js.map +1 -0
- package/dist/core/cli.d.ts +11 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +197 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +111 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cost-tracker.d.ts +22 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +41 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/evidence.d.ts +28 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +95 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/fix-loop.d.ts +29 -0
- package/dist/core/fix-loop.d.ts.map +1 -0
- package/dist/core/fix-loop.js +70 -0
- package/dist/core/fix-loop.js.map +1 -0
- package/dist/core/health-check.d.ts +21 -0
- package/dist/core/health-check.d.ts.map +1 -0
- package/dist/core/health-check.js +26 -0
- package/dist/core/health-check.js.map +1 -0
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +435 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/license-gen.d.ts +12 -0
- package/dist/core/license-gen.d.ts.map +1 -0
- package/dist/core/license-gen.js +169 -0
- package/dist/core/license-gen.js.map +1 -0
- package/dist/core/license.d.ts +33 -0
- package/dist/core/license.d.ts.map +1 -0
- package/dist/core/license.js +170 -0
- package/dist/core/license.js.map +1 -0
- package/dist/core/messages.d.ts +10 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js +47 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/multi-browser.d.ts +36 -0
- package/dist/core/multi-browser.d.ts.map +1 -0
- package/dist/core/multi-browser.js +88 -0
- package/dist/core/multi-browser.js.map +1 -0
- package/dist/core/orchestrator.d.ts +48 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +291 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/phase-gate.d.ts +4 -0
- package/dist/core/phase-gate.d.ts.map +1 -0
- package/dist/core/phase-gate.js +39 -0
- package/dist/core/phase-gate.js.map +1 -0
- package/dist/core/report-html.d.ts +9 -0
- package/dist/core/report-html.d.ts.map +1 -0
- package/dist/core/report-html.js +617 -0
- package/dist/core/report-html.js.map +1 -0
- package/dist/core/report-upload.d.ts +16 -0
- package/dist/core/report-upload.d.ts.map +1 -0
- package/dist/core/report-upload.js +124 -0
- package/dist/core/report-upload.js.map +1 -0
- package/dist/core/run-counter.d.ts +40 -0
- package/dist/core/run-counter.d.ts.map +1 -0
- package/dist/core/run-counter.js +120 -0
- package/dist/core/run-counter.js.map +1 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/helpers/api-client.d.ts +30 -0
- package/dist/helpers/api-client.d.ts.map +1 -0
- package/dist/helpers/api-client.js +77 -0
- package/dist/helpers/api-client.js.map +1 -0
- package/dist/helpers/element-discovery.d.ts +18 -0
- package/dist/helpers/element-discovery.d.ts.map +1 -0
- package/dist/helpers/element-discovery.js +82 -0
- package/dist/helpers/element-discovery.js.map +1 -0
- package/dist/helpers/env-resolver.d.ts +29 -0
- package/dist/helpers/env-resolver.d.ts.map +1 -0
- package/dist/helpers/env-resolver.js +51 -0
- package/dist/helpers/env-resolver.js.map +1 -0
- package/dist/helpers/form-filler.d.ts +13 -0
- package/dist/helpers/form-filler.d.ts.map +1 -0
- package/dist/helpers/form-filler.js +98 -0
- package/dist/helpers/form-filler.js.map +1 -0
- package/dist/helpers/modal-handler.d.ts +16 -0
- package/dist/helpers/modal-handler.d.ts.map +1 -0
- package/dist/helpers/modal-handler.js +95 -0
- package/dist/helpers/modal-handler.js.map +1 -0
- package/dist/helpers/navigation.d.ts +37 -0
- package/dist/helpers/navigation.d.ts.map +1 -0
- package/dist/helpers/navigation.js +83 -0
- package/dist/helpers/navigation.js.map +1 -0
- package/dist/helpers/quality-gate.d.ts +17 -0
- package/dist/helpers/quality-gate.d.ts.map +1 -0
- package/dist/helpers/quality-gate.js +144 -0
- package/dist/helpers/quality-gate.js.map +1 -0
- package/dist/helpers/screenshot.d.ts +24 -0
- package/dist/helpers/screenshot.d.ts.map +1 -0
- package/dist/helpers/screenshot.js +76 -0
- package/dist/helpers/screenshot.js.map +1 -0
- package/dist/helpers/seed-validator.d.ts +15 -0
- package/dist/helpers/seed-validator.d.ts.map +1 -0
- package/dist/helpers/seed-validator.js +53 -0
- package/dist/helpers/seed-validator.js.map +1 -0
- package/helpers/__tests__/api-client.test.ts +199 -0
- package/helpers/__tests__/element-discovery.test.ts +202 -0
- package/helpers/__tests__/form-filler-extended.test.ts +212 -0
- package/helpers/__tests__/form-filler.test.ts +99 -0
- package/helpers/__tests__/modal-handler.test.ts +152 -0
- package/helpers/__tests__/navigation.test.ts +214 -0
- package/helpers/__tests__/quality-gate.test.ts +117 -0
- package/helpers/__tests__/screenshot.test.ts +139 -0
- package/helpers/__tests__/seed-validator.test.ts +114 -0
- package/helpers/api-client.ts +111 -0
- package/helpers/element-discovery.ts +105 -0
- package/helpers/env-resolver.ts +69 -0
- package/helpers/form-filler.ts +126 -0
- package/helpers/modal-handler.ts +108 -0
- package/helpers/navigation.ts +100 -0
- package/helpers/quality-gate.ts +180 -0
- package/helpers/screenshot.ts +111 -0
- package/helpers/seed-validator.ts +70 -0
- package/package.json +88 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import type { Finding } from '../core/types';
|
|
5
|
+
import { ApiClient } from '../helpers/api-client';
|
|
6
|
+
|
|
7
|
+
/** A parsed HTTP endpoint extracted from a route file. */
|
|
8
|
+
interface ParsedEndpoint {
|
|
9
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
10
|
+
path: string;
|
|
11
|
+
sourceFile: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type HttpMethod = ParsedEndpoint['method'];
|
|
15
|
+
|
|
16
|
+
/** Regex patterns to locate route registrations in Express/NestJS route files. */
|
|
17
|
+
function buildRouterPattern(verb: string): RegExp {
|
|
18
|
+
return new RegExp(
|
|
19
|
+
`router\\.${verb}\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`,
|
|
20
|
+
'gi',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildDecoratorPattern(decorator: string): RegExp {
|
|
25
|
+
return new RegExp(
|
|
26
|
+
`@${decorator}\\s*\\(\\s*['"\`]([^'"\`]*)['"\`]\\s*\\)`,
|
|
27
|
+
'g',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ROUTES_DIR = join(process.cwd(), 'apps', 'api', 'src', 'routes');
|
|
32
|
+
const RESPONSE_TIME_THRESHOLD_MS = 500;
|
|
33
|
+
|
|
34
|
+
export class ApiExerciserAgent extends BaseAgent {
|
|
35
|
+
readonly agentId = 6;
|
|
36
|
+
readonly agentName = 'API Exerciser';
|
|
37
|
+
|
|
38
|
+
protected async preFlight(): Promise<void> {
|
|
39
|
+
const env = this.config.environments['alpha'] ?? Object.values(this.config.environments)[0];
|
|
40
|
+
if (!env) {
|
|
41
|
+
throw new Error('No environment configured — cannot determine API base URL');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(env.baseUrl, {
|
|
46
|
+
method: 'HEAD',
|
|
47
|
+
signal: AbortSignal.timeout(10_000),
|
|
48
|
+
});
|
|
49
|
+
if (response.status >= 500) {
|
|
50
|
+
throw new Error(`API base URL returned ${response.status}`);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`API base URL is not reachable: ${env.baseUrl} — ${err instanceof Error ? err.message : String(err)}`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
protected async execute(): Promise<Finding[]> {
|
|
60
|
+
const findings: Finding[] = [];
|
|
61
|
+
|
|
62
|
+
const env = this.config.environments['alpha'] ?? Object.values(this.config.environments)[0];
|
|
63
|
+
if (!env) {
|
|
64
|
+
findings.push({
|
|
65
|
+
id: `${this.agentId}-no-env`,
|
|
66
|
+
type: 'infra-issue',
|
|
67
|
+
severity: 'critical',
|
|
68
|
+
agentId: this.agentId,
|
|
69
|
+
module: 'api-exerciser',
|
|
70
|
+
description: 'No environment configured — cannot determine API base URL',
|
|
71
|
+
});
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const endpoints = await this.discoverEndpoints();
|
|
76
|
+
|
|
77
|
+
const credentials = this.config.auth.credentials?.['admin'] ??
|
|
78
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
79
|
+
|
|
80
|
+
let authedClient: ApiClient | null = null;
|
|
81
|
+
if (credentials) {
|
|
82
|
+
try {
|
|
83
|
+
authedClient = await ApiClient.createAuthenticated(env.baseUrl, credentials);
|
|
84
|
+
} catch {
|
|
85
|
+
// Non-fatal — auth tests will still run via unauthenticated client
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const anonClient = new ApiClient(env.baseUrl);
|
|
90
|
+
|
|
91
|
+
for (const endpoint of endpoints) {
|
|
92
|
+
// Happy path: authenticated request should return 2xx
|
|
93
|
+
if (authedClient) {
|
|
94
|
+
const happyFindings = await this.runHappyPath(authedClient, endpoint);
|
|
95
|
+
findings.push(...happyFindings);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Auth check: request without token should return 401
|
|
99
|
+
const authFindings = await this.runAuthCheck(anonClient, endpoint);
|
|
100
|
+
findings.push(...authFindings);
|
|
101
|
+
|
|
102
|
+
// Validation check: POST/PUT with empty body should return 400
|
|
103
|
+
if (endpoint.method === 'POST' || endpoint.method === 'PUT') {
|
|
104
|
+
const validationFindings = await this.runValidationCheck(anonClient, endpoint);
|
|
105
|
+
findings.push(...validationFindings);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private async discoverEndpoints(): Promise<ParsedEndpoint[]> {
|
|
113
|
+
const endpoints: ParsedEndpoint[] = [];
|
|
114
|
+
|
|
115
|
+
let files: string[];
|
|
116
|
+
try {
|
|
117
|
+
const entries = await readdir(ROUTES_DIR);
|
|
118
|
+
files = entries.filter(f => f.endsWith('.ts') && !f.endsWith('.test.ts'));
|
|
119
|
+
} catch {
|
|
120
|
+
return endpoints;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const filePath = join(ROUTES_DIR, file);
|
|
125
|
+
let content: string;
|
|
126
|
+
try {
|
|
127
|
+
content = await readFile(filePath, 'utf-8');
|
|
128
|
+
} catch {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const fileEndpoints = this.parseRouteFile(content, file);
|
|
133
|
+
endpoints.push(...fileEndpoints);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return endpoints;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parseRouteFile(content: string, sourceFile: string): ParsedEndpoint[] {
|
|
140
|
+
const endpoints: ParsedEndpoint[] = [];
|
|
141
|
+
const methodVerbs: Array<[HttpMethod, string, string]> = [
|
|
142
|
+
['GET', 'get', 'Get'],
|
|
143
|
+
['POST', 'post', 'Post'],
|
|
144
|
+
['PUT', 'put', 'Put'],
|
|
145
|
+
['DELETE', 'delete', 'Delete'],
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (const [method, verbLower, decoratorName] of methodVerbs) {
|
|
149
|
+
// Match router.get/post/put/delete('path', ...) patterns
|
|
150
|
+
const routerPattern = buildRouterPattern(verbLower);
|
|
151
|
+
let match = routerPattern.exec(content);
|
|
152
|
+
while (match !== null) {
|
|
153
|
+
const path = match[1];
|
|
154
|
+
if (path) {
|
|
155
|
+
endpoints.push({ method, path, sourceFile });
|
|
156
|
+
}
|
|
157
|
+
match = routerPattern.exec(content);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Match @Get/@Post/@Put/@Delete('path') NestJS decorator patterns
|
|
161
|
+
const decoratorPattern = buildDecoratorPattern(decoratorName);
|
|
162
|
+
let dMatch = decoratorPattern.exec(content);
|
|
163
|
+
while (dMatch !== null) {
|
|
164
|
+
const path = dMatch[1] ?? '';
|
|
165
|
+
endpoints.push({ method, path: path || '/', sourceFile });
|
|
166
|
+
dMatch = decoratorPattern.exec(content);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return endpoints;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async runHappyPath(client: ApiClient, endpoint: ParsedEndpoint): Promise<Finding[]> {
|
|
174
|
+
const findings: Finding[] = [];
|
|
175
|
+
const normalizedPath = this.normalizePath(endpoint.path);
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
let response;
|
|
179
|
+
switch (endpoint.method) {
|
|
180
|
+
case 'GET':
|
|
181
|
+
response = await client.get(normalizedPath);
|
|
182
|
+
break;
|
|
183
|
+
case 'POST':
|
|
184
|
+
response = await client.post(normalizedPath, { test: true });
|
|
185
|
+
break;
|
|
186
|
+
case 'PUT':
|
|
187
|
+
response = await client.put(normalizedPath, { test: true });
|
|
188
|
+
break;
|
|
189
|
+
case 'DELETE':
|
|
190
|
+
response = await client.delete(normalizedPath);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (response.status >= 400 && response.status !== 401 && response.status !== 404) {
|
|
195
|
+
findings.push({
|
|
196
|
+
id: `${this.agentId}-happy-${endpoint.method}-${normalizedPath.replace(/\//g, '_')}`,
|
|
197
|
+
type: 'code-bug-logic',
|
|
198
|
+
severity: 'high',
|
|
199
|
+
agentId: this.agentId,
|
|
200
|
+
module: endpoint.sourceFile,
|
|
201
|
+
description: `[API Exerciser] ${endpoint.method} ${normalizedPath} returned unexpected ${response.status} (expected 2xx)`,
|
|
202
|
+
file: endpoint.sourceFile,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (response.durationMs > RESPONSE_TIME_THRESHOLD_MS) {
|
|
207
|
+
findings.push({
|
|
208
|
+
id: `${this.agentId}-slow-${endpoint.method}-${normalizedPath.replace(/\//g, '_')}`,
|
|
209
|
+
type: 'code-bug-logic',
|
|
210
|
+
severity: 'medium',
|
|
211
|
+
agentId: this.agentId,
|
|
212
|
+
module: endpoint.sourceFile,
|
|
213
|
+
description: `[API Exerciser] ${endpoint.method} ${normalizedPath} responded in ${response.durationMs}ms (threshold: ${RESPONSE_TIME_THRESHOLD_MS}ms)`,
|
|
214
|
+
file: endpoint.sourceFile,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Network-level failure is non-fatal for individual endpoints
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return findings;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private async runAuthCheck(client: ApiClient, endpoint: ParsedEndpoint): Promise<Finding[]> {
|
|
225
|
+
const findings: Finding[] = [];
|
|
226
|
+
const normalizedPath = this.normalizePath(endpoint.path);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
let response;
|
|
230
|
+
switch (endpoint.method) {
|
|
231
|
+
case 'GET':
|
|
232
|
+
response = await client.get(normalizedPath);
|
|
233
|
+
break;
|
|
234
|
+
case 'POST':
|
|
235
|
+
response = await client.post(normalizedPath, {});
|
|
236
|
+
break;
|
|
237
|
+
case 'PUT':
|
|
238
|
+
response = await client.put(normalizedPath, {});
|
|
239
|
+
break;
|
|
240
|
+
case 'DELETE':
|
|
241
|
+
response = await client.delete(normalizedPath);
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const isPublicRoute =
|
|
246
|
+
normalizedPath.includes('/auth') ||
|
|
247
|
+
normalizedPath.includes('/health') ||
|
|
248
|
+
normalizedPath.includes('/public');
|
|
249
|
+
|
|
250
|
+
if (response.status !== 401 && !isPublicRoute) {
|
|
251
|
+
findings.push({
|
|
252
|
+
id: `${this.agentId}-auth-${endpoint.method}-${normalizedPath.replace(/\//g, '_')}`,
|
|
253
|
+
type: 'code-bug-security',
|
|
254
|
+
severity: 'high',
|
|
255
|
+
agentId: this.agentId,
|
|
256
|
+
module: endpoint.sourceFile,
|
|
257
|
+
description:
|
|
258
|
+
`[API Exerciser] ${endpoint.method} ${normalizedPath} did not return 401 for unauthenticated request (got ${response.status})`,
|
|
259
|
+
file: endpoint.sourceFile,
|
|
260
|
+
suggestedFix: 'Ensure authentication guard is applied to this endpoint',
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
// Network-level failure
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return findings;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async runValidationCheck(
|
|
271
|
+
client: ApiClient,
|
|
272
|
+
endpoint: ParsedEndpoint,
|
|
273
|
+
): Promise<Finding[]> {
|
|
274
|
+
const findings: Finding[] = [];
|
|
275
|
+
const normalizedPath = this.normalizePath(endpoint.path);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const response =
|
|
279
|
+
endpoint.method === 'POST'
|
|
280
|
+
? await client.post(normalizedPath, {})
|
|
281
|
+
: await client.put(normalizedPath, {});
|
|
282
|
+
|
|
283
|
+
// 401 is acceptable (auth guard fires before validation); 400 is the target
|
|
284
|
+
if (response.status !== 400 && response.status !== 401) {
|
|
285
|
+
findings.push({
|
|
286
|
+
id: `${this.agentId}-validation-${endpoint.method}-${normalizedPath.replace(/\//g, '_')}`,
|
|
287
|
+
type: 'code-bug-logic',
|
|
288
|
+
severity: 'medium',
|
|
289
|
+
agentId: this.agentId,
|
|
290
|
+
module: endpoint.sourceFile,
|
|
291
|
+
description:
|
|
292
|
+
`[API Exerciser] ${endpoint.method} ${normalizedPath} with empty body returned ${response.status} instead of 400`,
|
|
293
|
+
file: endpoint.sourceFile,
|
|
294
|
+
suggestedFix:
|
|
295
|
+
'Add input validation (e.g. class-validator DTO) to reject empty bodies',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
} catch {
|
|
299
|
+
// Network-level failure
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return findings;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Replace path parameters like :id or {id} with placeholder values. */
|
|
306
|
+
private normalizePath(path: string): string {
|
|
307
|
+
return path
|
|
308
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, '1')
|
|
309
|
+
.replace(/\{[^}]+\}/g, '1');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import { BaseAgent } from './base-agent';
|
|
3
|
+
import type { Finding, FindingSeverity } from '../core/types';
|
|
4
|
+
import { ApiClient } from '../helpers/api-client';
|
|
5
|
+
|
|
6
|
+
/** Shape of a single vulnerability entry in `pnpm audit --json` output. */
|
|
7
|
+
interface AuditVulnerability {
|
|
8
|
+
severity: string;
|
|
9
|
+
via?: Array<string | { title?: string; severity?: string }>;
|
|
10
|
+
fixAvailable?: boolean | { name: string; version: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Top-level shape of `pnpm audit --json` output. */
|
|
14
|
+
interface AuditReport {
|
|
15
|
+
vulnerabilities?: Record<string, AuditVulnerability>;
|
|
16
|
+
metadata?: {
|
|
17
|
+
vulnerabilities?: {
|
|
18
|
+
critical?: number;
|
|
19
|
+
high?: number;
|
|
20
|
+
moderate?: number;
|
|
21
|
+
low?: number;
|
|
22
|
+
info?: number;
|
|
23
|
+
total?: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Security headers that must be present on every HTTP response. */
|
|
29
|
+
const REQUIRED_SECURITY_HEADERS: ReadonlyArray<string> = [
|
|
30
|
+
'content-security-policy',
|
|
31
|
+
'x-frame-options',
|
|
32
|
+
'strict-transport-security',
|
|
33
|
+
'x-content-type-options',
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function execFilePromise(
|
|
37
|
+
file: string,
|
|
38
|
+
args: string[],
|
|
39
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
execFile(file, args, { shell: true }, (error, stdout, stderr) => {
|
|
42
|
+
if (error) {
|
|
43
|
+
// pnpm audit exits with non-zero when vulnerabilities exist; capture stdout anyway
|
|
44
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Map pnpm/npm audit severity strings to FindingSeverity values. */
|
|
53
|
+
function mapAuditSeverity(auditSeverity: string): FindingSeverity {
|
|
54
|
+
switch (auditSeverity.toLowerCase()) {
|
|
55
|
+
case 'critical': return 'critical';
|
|
56
|
+
case 'high': return 'high';
|
|
57
|
+
case 'moderate': return 'medium';
|
|
58
|
+
case 'low': return 'low';
|
|
59
|
+
default: return 'low';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SecurityScoutAgent extends BaseAgent {
|
|
64
|
+
readonly agentId = 7;
|
|
65
|
+
readonly agentName = 'Security Scout';
|
|
66
|
+
|
|
67
|
+
protected async preFlight(): Promise<void> {
|
|
68
|
+
await execFilePromise('pnpm', ['--version']);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected async execute(): Promise<Finding[]> {
|
|
72
|
+
const findings: Finding[] = [];
|
|
73
|
+
|
|
74
|
+
const auditFindings = await this.runDependencyAudit();
|
|
75
|
+
findings.push(...auditFindings);
|
|
76
|
+
|
|
77
|
+
const headerFindings = await this.checkSecurityHeaders();
|
|
78
|
+
findings.push(...headerFindings);
|
|
79
|
+
|
|
80
|
+
return findings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async runDependencyAudit(): Promise<Finding[]> {
|
|
84
|
+
const findings: Finding[] = [];
|
|
85
|
+
|
|
86
|
+
let stdout: string;
|
|
87
|
+
try {
|
|
88
|
+
const result = await execFilePromise('pnpm', ['audit', '--json']);
|
|
89
|
+
stdout = result.stdout;
|
|
90
|
+
} catch {
|
|
91
|
+
findings.push({
|
|
92
|
+
id: `${this.agentId}-audit-failed`,
|
|
93
|
+
type: 'infra-issue',
|
|
94
|
+
severity: 'medium',
|
|
95
|
+
agentId: this.agentId,
|
|
96
|
+
module: 'security-scout',
|
|
97
|
+
description: '[Security Scout] pnpm audit failed to run — dependency vulnerabilities could not be assessed',
|
|
98
|
+
});
|
|
99
|
+
return findings;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!stdout.trim()) {
|
|
103
|
+
return findings;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let report: AuditReport;
|
|
107
|
+
try {
|
|
108
|
+
report = JSON.parse(stdout) as AuditReport;
|
|
109
|
+
} catch {
|
|
110
|
+
findings.push({
|
|
111
|
+
id: `${this.agentId}-audit-parse-error`,
|
|
112
|
+
type: 'infra-issue',
|
|
113
|
+
severity: 'medium',
|
|
114
|
+
agentId: this.agentId,
|
|
115
|
+
module: 'security-scout',
|
|
116
|
+
description: '[Security Scout] pnpm audit JSON output could not be parsed',
|
|
117
|
+
});
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const vulnerabilities = report.vulnerabilities ?? {};
|
|
122
|
+
|
|
123
|
+
for (const [pkgName, vuln] of Object.entries(vulnerabilities)) {
|
|
124
|
+
const severity = mapAuditSeverity(vuln.severity);
|
|
125
|
+
|
|
126
|
+
const viaDescriptions = (vuln.via ?? [])
|
|
127
|
+
.map(v => (typeof v === 'string' ? v : (v.title ?? 'unknown')))
|
|
128
|
+
.join(', ');
|
|
129
|
+
|
|
130
|
+
findings.push({
|
|
131
|
+
id: `${this.agentId}-vuln-${pkgName.replace(/[^a-zA-Z0-9_-]/g, '_')}`,
|
|
132
|
+
type: 'code-bug-security',
|
|
133
|
+
severity,
|
|
134
|
+
agentId: this.agentId,
|
|
135
|
+
module: 'dependencies',
|
|
136
|
+
description: `[Security Scout] Vulnerability in "${pkgName}" (${vuln.severity}): ${viaDescriptions || 'see audit output'}`,
|
|
137
|
+
suggestedFix: vuln.fixAvailable
|
|
138
|
+
? `Run \`pnpm audit fix\` — fix is available`
|
|
139
|
+
: 'No automatic fix available; review and update manually',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return findings;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private async checkSecurityHeaders(): Promise<Finding[]> {
|
|
147
|
+
const findings: Finding[] = [];
|
|
148
|
+
|
|
149
|
+
const env = this.config.environments['alpha'] ?? Object.values(this.config.environments)[0];
|
|
150
|
+
if (!env) {
|
|
151
|
+
return findings;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const client = new ApiClient(env.baseUrl);
|
|
155
|
+
|
|
156
|
+
let response;
|
|
157
|
+
try {
|
|
158
|
+
response = await client.get('/');
|
|
159
|
+
} catch {
|
|
160
|
+
findings.push({
|
|
161
|
+
id: `${this.agentId}-headers-unreachable`,
|
|
162
|
+
type: 'infra-issue',
|
|
163
|
+
severity: 'medium',
|
|
164
|
+
agentId: this.agentId,
|
|
165
|
+
module: 'security-scout',
|
|
166
|
+
description: `[Security Scout] Could not reach ${env.baseUrl}/ to check security headers`,
|
|
167
|
+
});
|
|
168
|
+
return findings;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const header of REQUIRED_SECURITY_HEADERS) {
|
|
172
|
+
const headerValue = response.headers[header] ?? response.headers[header.toLowerCase()];
|
|
173
|
+
if (!headerValue) {
|
|
174
|
+
findings.push({
|
|
175
|
+
id: `${this.agentId}-missing-header-${header.replace(/[^a-zA-Z0-9]/g, '_')}`,
|
|
176
|
+
type: 'code-bug-security',
|
|
177
|
+
severity: 'medium',
|
|
178
|
+
agentId: this.agentId,
|
|
179
|
+
module: 'security-headers',
|
|
180
|
+
description: `[Security Scout] Missing security header: ${header}`,
|
|
181
|
+
suggestedFix: `Add the "${header}" response header to all HTTP responses`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return findings;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
|
+
import type { Browser, Page } from 'playwright';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import type { Finding, FindingSeverity } from '../core/types';
|
|
5
|
+
import { login, navigateToSection, waitForSectionLoad } from '../helpers/navigation';
|
|
6
|
+
import { captureScreenshot } from '../helpers/screenshot';
|
|
7
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
8
|
+
|
|
9
|
+
/** Subset of the axe-core AxeResults shape we need. */
|
|
10
|
+
interface AxeResults {
|
|
11
|
+
violations: Array<{
|
|
12
|
+
id: string;
|
|
13
|
+
impact?: string | null;
|
|
14
|
+
description: string;
|
|
15
|
+
help: string;
|
|
16
|
+
helpUrl: string;
|
|
17
|
+
nodes: Array<{ html: string }>;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function execFilePromise(
|
|
22
|
+
file: string,
|
|
23
|
+
args: string[],
|
|
24
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
execFile(file, args, { shell: true }, (error, stdout, stderr) => {
|
|
27
|
+
if (error) {
|
|
28
|
+
reject(error);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '' });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Map axe-core impact values to FindingSeverity. */
|
|
37
|
+
export function mapAxeImpact(impact: string | null): FindingSeverity {
|
|
38
|
+
switch ((impact ?? '').toLowerCase()) {
|
|
39
|
+
case 'critical': return 'critical';
|
|
40
|
+
case 'serious': return 'high';
|
|
41
|
+
case 'moderate': return 'medium';
|
|
42
|
+
case 'minor': return 'low';
|
|
43
|
+
default: return 'low';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class A11yGuardianAgent extends BaseAgent {
|
|
48
|
+
readonly agentId = 8;
|
|
49
|
+
readonly agentName = 'A11y Guardian';
|
|
50
|
+
|
|
51
|
+
protected async preFlight(): Promise<void> {
|
|
52
|
+
await execFilePromise('npx', ['playwright', '--version']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
protected async execute(): Promise<Finding[]> {
|
|
56
|
+
const findings: Finding[] = [];
|
|
57
|
+
|
|
58
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
59
|
+
if (!env) {
|
|
60
|
+
findings.push({
|
|
61
|
+
id: `${this.agentId}-no-env`,
|
|
62
|
+
type: 'infra-issue',
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
agentId: this.agentId,
|
|
65
|
+
module: 'a11y-guardian',
|
|
66
|
+
description: 'No environment configured — cannot perform accessibility scan',
|
|
67
|
+
});
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
72
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${env.baseUrl}${rawLoginUrl}`;
|
|
73
|
+
const credentials =
|
|
74
|
+
this.config.auth.credentials?.['admin'] ??
|
|
75
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
76
|
+
|
|
77
|
+
if (!credentials) {
|
|
78
|
+
findings.push({
|
|
79
|
+
id: `${this.agentId}-no-credentials`,
|
|
80
|
+
type: 'infra-issue',
|
|
81
|
+
severity: 'critical',
|
|
82
|
+
agentId: this.agentId,
|
|
83
|
+
module: 'a11y-guardian',
|
|
84
|
+
description: 'No credentials configured — cannot log in for accessibility scan',
|
|
85
|
+
});
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { chromium } = await import('playwright');
|
|
90
|
+
const { AxeBuilder } = await import('@axe-core/playwright');
|
|
91
|
+
|
|
92
|
+
let browser: Browser | null = null;
|
|
93
|
+
let page: Page | null = null;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
browser = await chromium.launch({ headless: true });
|
|
97
|
+
page = await browser.newPage();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await login(page, credentials, loginUrl);
|
|
101
|
+
} catch (loginError) {
|
|
102
|
+
findings.push({
|
|
103
|
+
id: `${this.agentId}-login-failed`,
|
|
104
|
+
type: 'infra-issue',
|
|
105
|
+
severity: 'medium',
|
|
106
|
+
agentId: this.agentId,
|
|
107
|
+
module: 'auth',
|
|
108
|
+
description: `Login failed (will scan anonymously): ${loginError instanceof Error ? loginError.message.split('\n')[0] : String(loginError)}`,
|
|
109
|
+
});
|
|
110
|
+
// Navigate to baseUrl so anonymous scanning can proceed
|
|
111
|
+
await page.goto(env.baseUrl, { waitUntil: 'networkidle', timeout: 10000 }).catch(() => {});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (const module of this.config.modules) {
|
|
115
|
+
try {
|
|
116
|
+
await navigateToSection(page, module.id, this.config);
|
|
117
|
+
await waitForSectionLoad(page);
|
|
118
|
+
} catch (navErr) {
|
|
119
|
+
findings.push({
|
|
120
|
+
id: `${this.agentId}-nav-${module.id}`,
|
|
121
|
+
type: 'infra-issue',
|
|
122
|
+
severity: 'high',
|
|
123
|
+
agentId: this.agentId,
|
|
124
|
+
module: module.id,
|
|
125
|
+
description: `[A11y] Navigation to module "${module.id}" failed: ${navErr instanceof Error ? navErr.message : String(navErr)}`,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const screenshotPath = await captureScreenshot(
|
|
131
|
+
page,
|
|
132
|
+
`a11y-${module.id}`,
|
|
133
|
+
this.runDir,
|
|
134
|
+
);
|
|
135
|
+
this.addEvidence({
|
|
136
|
+
type: 'screenshot',
|
|
137
|
+
path: screenshotPath,
|
|
138
|
+
description: `A11y scan screenshot for module "${module.id}"`,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
let axeResults: AxeResults;
|
|
142
|
+
try {
|
|
143
|
+
axeResults = await new AxeBuilder({ page }).analyze();
|
|
144
|
+
} catch (axeErr) {
|
|
145
|
+
findings.push({
|
|
146
|
+
id: `${this.agentId}-axe-error-${module.id}`,
|
|
147
|
+
type: 'infra-issue',
|
|
148
|
+
severity: 'medium',
|
|
149
|
+
agentId: this.agentId,
|
|
150
|
+
module: module.id,
|
|
151
|
+
description: `[A11y] axe-core scan failed for module "${module.id}": ${axeErr instanceof Error ? axeErr.message : String(axeErr)}`,
|
|
152
|
+
});
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const violation of axeResults.violations) {
|
|
157
|
+
const severity = mapAxeImpact(violation.impact ?? null);
|
|
158
|
+
const nodeCount = violation.nodes.length;
|
|
159
|
+
|
|
160
|
+
findings.push({
|
|
161
|
+
id: `${this.agentId}-axe-${module.id}-${violation.id}`,
|
|
162
|
+
type: 'code-bug-logic',
|
|
163
|
+
severity,
|
|
164
|
+
agentId: this.agentId,
|
|
165
|
+
module: module.id,
|
|
166
|
+
description:
|
|
167
|
+
`[A11y] ${violation.description} (rule: ${violation.id}, ${nodeCount} node${nodeCount !== 1 ? 's' : ''})`,
|
|
168
|
+
screenshot: screenshotPath,
|
|
169
|
+
suggestedFix: violation.help,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Optional: Lighthouse CI
|
|
175
|
+
await this.runLighthouseIfAvailable();
|
|
176
|
+
|
|
177
|
+
// Optional: Pa11y CI
|
|
178
|
+
await this.runPa11yIfAvailable();
|
|
179
|
+
} finally {
|
|
180
|
+
if (page) await page.close().catch(() => undefined);
|
|
181
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return findings;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async runLighthouseIfAvailable(): Promise<void> {
|
|
188
|
+
try {
|
|
189
|
+
await execFilePromise('npx', ['lhci', 'autorun']);
|
|
190
|
+
this.addEvidence({
|
|
191
|
+
type: 'report',
|
|
192
|
+
path: 'lhci-report',
|
|
193
|
+
description: 'Lighthouse CI accessibility report',
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
// Lighthouse not available or failed — non-fatal, skip silently
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async runPa11yIfAvailable(): Promise<void> {
|
|
201
|
+
try {
|
|
202
|
+
await execFilePromise('npx', ['pa11y-ci']);
|
|
203
|
+
this.addEvidence({
|
|
204
|
+
type: 'report',
|
|
205
|
+
path: 'pa11y-report',
|
|
206
|
+
description: 'Pa11y CI accessibility report',
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
// Pa11y not available or failed — non-fatal, skip silently
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|