@avi770/testteam 1.2.0 → 2.0.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 +29 -1
- package/README.md +53 -6
- package/agents/24-signup-onboarding-tester.ts +429 -0
- package/agents/25-crud-flow-tester.ts +302 -0
- package/agents/26-form-validator.ts +297 -0
- package/agents/27-search-filter-tester.ts +326 -0
- package/agents/28-navigation-routing-tester.ts +425 -0
- package/agents/29-responsive-interaction-tester.ts +350 -0
- package/agents/30-multi-user-scenario-tester.ts +319 -0
- package/agents/31-load-tester.ts +134 -0
- package/agents/32-memory-leak-detector.ts +194 -0
- package/agents/33-bundle-analyzer.ts +132 -0
- package/agents/34-xss-scanner.ts +191 -0
- package/agents/35-csrf-tester.ts +82 -0
- package/agents/36-auth-fuzzer.ts +194 -0
- package/agents/37-dependency-scanner.ts +176 -0
- package/agents/38-secrets-scanner.ts +137 -0
- package/agents/39-api-contract-tester.ts +199 -0
- package/agents/40-rate-limit-tester.ts +94 -0
- package/agents/41-api-pagination-tester.ts +97 -0
- package/agents/42-graphql-tester.ts +222 -0
- package/agents/43-data-consistency-checker.ts +205 -0
- package/agents/44-backup-recovery-tester.ts +152 -0
- package/agents/45-data-privacy-scanner.ts +125 -0
- package/agents/46-seo-auditor.ts +294 -0
- package/agents/47-social-preview-tester.ts +232 -0
- package/agents/48-lighthouse-auditor.ts +213 -0
- package/agents/49-i18n-tester.ts +198 -0
- package/agents/50-timezone-tester.ts +173 -0
- package/agents/51-error-recovery-tester.ts +155 -0
- package/agents/52-offline-mode-tester.ts +180 -0
- package/agents/53-graceful-degradation-tester.ts +156 -0
- package/agents/54-websocket-tester.ts +151 -0
- package/agents/55-realtime-sync-tester.ts +194 -0
- package/agents/56-file-upload-tester.ts +194 -0
- package/agents/57-export-tester.ts +174 -0
- package/agents/58-payment-flow-tester.ts +183 -0
- package/agents/59-ssl-tls-auditor.ts +141 -0
- package/agents/60-dns-cdn-tester.ts +117 -0
- package/agents/61-docker-health-checker.ts +111 -0
- package/agents/62-env-config-validator.ts +152 -0
- package/agents/63-log-quality-auditor.ts +136 -0
- package/agents/64-analytics-tracker-tester.ts +165 -0
- package/agents/65-gdpr-compliance-tester.ts +215 -0
- package/agents/66-soc2-control-validator.ts +210 -0
- package/agents/67-wcag-aaa-tester.ts +241 -0
- package/agents/68-dead-code-detector.ts +135 -0
- package/agents/69-type-safety-auditor.ts +164 -0
- package/agents/70-complexity-analyzer.ts +179 -0
- package/agents/__tests__/24-signup-onboarding-tester.test.ts +274 -0
- package/agents/__tests__/25-crud-flow-tester.test.ts +322 -0
- package/agents/__tests__/26-form-validator.test.ts +345 -0
- package/agents/__tests__/27-search-filter-tester.test.ts +311 -0
- package/agents/__tests__/28-navigation-routing-tester.test.ts +328 -0
- package/agents/__tests__/29-responsive-interaction-tester.test.ts +297 -0
- package/agents/__tests__/30-multi-user-scenario-tester.test.ts +328 -0
- package/agents/__tests__/31-load-tester.test.ts +189 -0
- package/agents/__tests__/32-memory-leak-detector.test.ts +251 -0
- package/agents/__tests__/33-bundle-analyzer.test.ts +237 -0
- package/agents/__tests__/34-xss-scanner.test.ts +258 -0
- package/agents/__tests__/35-csrf-tester.test.ts +200 -0
- package/agents/__tests__/36-auth-fuzzer.test.ts +214 -0
- package/agents/__tests__/37-dependency-scanner.test.ts +266 -0
- package/agents/__tests__/38-secrets-scanner.test.ts +224 -0
- package/agents/__tests__/39-api-contract-tester.test.ts +312 -0
- package/agents/__tests__/40-rate-limit-tester.test.ts +192 -0
- package/agents/__tests__/41-api-pagination-tester.test.ts +198 -0
- package/agents/__tests__/42-graphql-tester.test.ts +252 -0
- package/agents/__tests__/43-data-consistency-checker.test.ts +232 -0
- package/agents/__tests__/44-backup-recovery-tester.test.ts +222 -0
- package/agents/__tests__/45-data-privacy-scanner.test.ts +223 -0
- package/agents/__tests__/46-seo-auditor.test.ts +261 -0
- package/agents/__tests__/47-social-preview-tester.test.ts +245 -0
- package/agents/__tests__/48-lighthouse-auditor.test.ts +276 -0
- package/agents/__tests__/49-i18n-tester.test.ts +201 -0
- package/agents/__tests__/50-timezone-tester.test.ts +172 -0
- package/agents/__tests__/51-error-recovery-tester.test.ts +162 -0
- package/agents/__tests__/52-offline-mode-tester.test.ts +164 -0
- package/agents/__tests__/53-graceful-degradation-tester.test.ts +168 -0
- package/agents/__tests__/54-websocket-tester.test.ts +157 -0
- package/agents/__tests__/55-realtime-sync-tester.test.ts +181 -0
- package/agents/__tests__/56-file-upload-tester.test.ts +172 -0
- package/agents/__tests__/57-export-tester.test.ts +169 -0
- package/agents/__tests__/58-payment-flow-tester.test.ts +182 -0
- package/agents/__tests__/59-ssl-tls-auditor.test.ts +179 -0
- package/agents/__tests__/60-dns-cdn-tester.test.ts +176 -0
- package/agents/__tests__/61-docker-health-checker.test.ts +150 -0
- package/agents/__tests__/62-env-config-validator.test.ts +166 -0
- package/agents/__tests__/63-log-quality-auditor.test.ts +175 -0
- package/agents/__tests__/64-analytics-tracker-tester.test.ts +158 -0
- package/agents/__tests__/65-gdpr-compliance-tester.test.ts +174 -0
- package/agents/__tests__/66-soc2-control-validator.test.ts +183 -0
- package/agents/__tests__/67-wcag-aaa-tester.test.ts +190 -0
- package/agents/__tests__/68-dead-code-detector.test.ts +174 -0
- package/agents/__tests__/69-type-safety-auditor.test.ts +173 -0
- package/agents/__tests__/70-complexity-analyzer.test.ts +177 -0
- package/agents/__tests__/registry.test.ts +13 -13
- package/agents/registry.ts +146 -5
- package/core/__tests__/integration.test.ts +4 -4
- package/core/__tests__/orchestrator.test.ts +17 -16
- package/core/license.ts +208 -211
- package/core/orchestrator.ts +6 -4
- package/dist/agents/24-signup-onboarding-tester.d.ts +35 -0
- package/dist/agents/24-signup-onboarding-tester.d.ts.map +1 -0
- package/dist/agents/24-signup-onboarding-tester.js +357 -0
- package/dist/agents/24-signup-onboarding-tester.js.map +1 -0
- package/dist/agents/25-crud-flow-tester.d.ts +11 -0
- package/dist/agents/25-crud-flow-tester.d.ts.map +1 -0
- package/dist/agents/25-crud-flow-tester.js +253 -0
- package/dist/agents/25-crud-flow-tester.js.map +1 -0
- package/dist/agents/26-form-validator.d.ts +12 -0
- package/dist/agents/26-form-validator.d.ts.map +1 -0
- package/dist/agents/26-form-validator.js +257 -0
- package/dist/agents/26-form-validator.js.map +1 -0
- package/dist/agents/27-search-filter-tester.d.ts +20 -0
- package/dist/agents/27-search-filter-tester.d.ts.map +1 -0
- package/dist/agents/27-search-filter-tester.js +267 -0
- package/dist/agents/27-search-filter-tester.js.map +1 -0
- package/dist/agents/28-navigation-routing-tester.d.ts +32 -0
- package/dist/agents/28-navigation-routing-tester.d.ts.map +1 -0
- package/dist/agents/28-navigation-routing-tester.js +363 -0
- package/dist/agents/28-navigation-routing-tester.js.map +1 -0
- package/dist/agents/29-responsive-interaction-tester.d.ts +26 -0
- package/dist/agents/29-responsive-interaction-tester.d.ts.map +1 -0
- package/dist/agents/29-responsive-interaction-tester.js +272 -0
- package/dist/agents/29-responsive-interaction-tester.js.map +1 -0
- package/dist/agents/30-multi-user-scenario-tester.d.ts +24 -0
- package/dist/agents/30-multi-user-scenario-tester.d.ts.map +1 -0
- package/dist/agents/30-multi-user-scenario-tester.js +254 -0
- package/dist/agents/30-multi-user-scenario-tester.js.map +1 -0
- package/dist/agents/31-load-tester.d.ts +12 -0
- package/dist/agents/31-load-tester.d.ts.map +1 -0
- package/dist/agents/31-load-tester.js +110 -0
- package/dist/agents/31-load-tester.js.map +1 -0
- package/dist/agents/32-memory-leak-detector.d.ts +12 -0
- package/dist/agents/32-memory-leak-detector.d.ts.map +1 -0
- package/dist/agents/32-memory-leak-detector.js +167 -0
- package/dist/agents/32-memory-leak-detector.js.map +1 -0
- package/dist/agents/33-bundle-analyzer.d.ts +10 -0
- package/dist/agents/33-bundle-analyzer.d.ts.map +1 -0
- package/dist/agents/33-bundle-analyzer.js +111 -0
- package/dist/agents/33-bundle-analyzer.js.map +1 -0
- package/dist/agents/34-xss-scanner.d.ts +11 -0
- package/dist/agents/34-xss-scanner.d.ts.map +1 -0
- package/dist/agents/34-xss-scanner.js +164 -0
- package/dist/agents/34-xss-scanner.js.map +1 -0
- package/dist/agents/35-csrf-tester.d.ts +11 -0
- package/dist/agents/35-csrf-tester.d.ts.map +1 -0
- package/dist/agents/35-csrf-tester.js +70 -0
- package/dist/agents/35-csrf-tester.js.map +1 -0
- package/dist/agents/36-auth-fuzzer.d.ts +13 -0
- package/dist/agents/36-auth-fuzzer.d.ts.map +1 -0
- package/dist/agents/36-auth-fuzzer.js +163 -0
- package/dist/agents/36-auth-fuzzer.js.map +1 -0
- package/dist/agents/37-dependency-scanner.d.ts +11 -0
- package/dist/agents/37-dependency-scanner.d.ts.map +1 -0
- package/dist/agents/37-dependency-scanner.js +139 -0
- package/dist/agents/37-dependency-scanner.js.map +1 -0
- package/dist/agents/38-secrets-scanner.d.ts +11 -0
- package/dist/agents/38-secrets-scanner.d.ts.map +1 -0
- package/dist/agents/38-secrets-scanner.js +116 -0
- package/dist/agents/38-secrets-scanner.js.map +1 -0
- package/dist/agents/39-api-contract-tester.d.ts +12 -0
- package/dist/agents/39-api-contract-tester.d.ts.map +1 -0
- package/dist/agents/39-api-contract-tester.js +142 -0
- package/dist/agents/39-api-contract-tester.js.map +1 -0
- package/dist/agents/40-rate-limit-tester.d.ts +12 -0
- package/dist/agents/40-rate-limit-tester.d.ts.map +1 -0
- package/dist/agents/40-rate-limit-tester.js +79 -0
- package/dist/agents/40-rate-limit-tester.js.map +1 -0
- package/dist/agents/41-api-pagination-tester.d.ts +12 -0
- package/dist/agents/41-api-pagination-tester.d.ts.map +1 -0
- package/dist/agents/41-api-pagination-tester.js +79 -0
- package/dist/agents/41-api-pagination-tester.js.map +1 -0
- package/dist/agents/42-graphql-tester.d.ts +13 -0
- package/dist/agents/42-graphql-tester.d.ts.map +1 -0
- package/dist/agents/42-graphql-tester.js +187 -0
- package/dist/agents/42-graphql-tester.js.map +1 -0
- package/dist/agents/43-data-consistency-checker.d.ts +11 -0
- package/dist/agents/43-data-consistency-checker.d.ts.map +1 -0
- package/dist/agents/43-data-consistency-checker.js +176 -0
- package/dist/agents/43-data-consistency-checker.js.map +1 -0
- package/dist/agents/44-backup-recovery-tester.d.ts +11 -0
- package/dist/agents/44-backup-recovery-tester.d.ts.map +1 -0
- package/dist/agents/44-backup-recovery-tester.js +128 -0
- package/dist/agents/44-backup-recovery-tester.js.map +1 -0
- package/dist/agents/45-data-privacy-scanner.d.ts +11 -0
- package/dist/agents/45-data-privacy-scanner.d.ts.map +1 -0
- package/dist/agents/45-data-privacy-scanner.js +100 -0
- package/dist/agents/45-data-privacy-scanner.js.map +1 -0
- package/dist/agents/46-seo-auditor.d.ts +12 -0
- package/dist/agents/46-seo-auditor.d.ts.map +1 -0
- package/dist/agents/46-seo-auditor.js +275 -0
- package/dist/agents/46-seo-auditor.js.map +1 -0
- package/dist/agents/47-social-preview-tester.d.ts +11 -0
- package/dist/agents/47-social-preview-tester.d.ts.map +1 -0
- package/dist/agents/47-social-preview-tester.js +219 -0
- package/dist/agents/47-social-preview-tester.js.map +1 -0
- package/dist/agents/48-lighthouse-auditor.d.ts +11 -0
- package/dist/agents/48-lighthouse-auditor.d.ts.map +1 -0
- package/dist/agents/48-lighthouse-auditor.js +192 -0
- package/dist/agents/48-lighthouse-auditor.js.map +1 -0
- package/dist/agents/49-i18n-tester.d.ts +13 -0
- package/dist/agents/49-i18n-tester.d.ts.map +1 -0
- package/dist/agents/49-i18n-tester.js +172 -0
- package/dist/agents/49-i18n-tester.js.map +1 -0
- package/dist/agents/50-timezone-tester.d.ts +11 -0
- package/dist/agents/50-timezone-tester.d.ts.map +1 -0
- package/dist/agents/50-timezone-tester.js +152 -0
- package/dist/agents/50-timezone-tester.js.map +1 -0
- package/dist/agents/51-error-recovery-tester.d.ts +11 -0
- package/dist/agents/51-error-recovery-tester.d.ts.map +1 -0
- package/dist/agents/51-error-recovery-tester.js +134 -0
- package/dist/agents/51-error-recovery-tester.js.map +1 -0
- package/dist/agents/52-offline-mode-tester.d.ts +12 -0
- package/dist/agents/52-offline-mode-tester.d.ts.map +1 -0
- package/dist/agents/52-offline-mode-tester.js +161 -0
- package/dist/agents/52-offline-mode-tester.js.map +1 -0
- package/dist/agents/53-graceful-degradation-tester.d.ts +12 -0
- package/dist/agents/53-graceful-degradation-tester.d.ts.map +1 -0
- package/dist/agents/53-graceful-degradation-tester.js +130 -0
- package/dist/agents/53-graceful-degradation-tester.js.map +1 -0
- package/dist/agents/54-websocket-tester.d.ts +10 -0
- package/dist/agents/54-websocket-tester.d.ts.map +1 -0
- package/dist/agents/54-websocket-tester.js +132 -0
- package/dist/agents/54-websocket-tester.js.map +1 -0
- package/dist/agents/55-realtime-sync-tester.d.ts +11 -0
- package/dist/agents/55-realtime-sync-tester.d.ts.map +1 -0
- package/dist/agents/55-realtime-sync-tester.js +175 -0
- package/dist/agents/55-realtime-sync-tester.js.map +1 -0
- package/dist/agents/56-file-upload-tester.d.ts +12 -0
- package/dist/agents/56-file-upload-tester.d.ts.map +1 -0
- package/dist/agents/56-file-upload-tester.js +166 -0
- package/dist/agents/56-file-upload-tester.js.map +1 -0
- package/dist/agents/57-export-tester.d.ts +11 -0
- package/dist/agents/57-export-tester.d.ts.map +1 -0
- package/dist/agents/57-export-tester.js +155 -0
- package/dist/agents/57-export-tester.js.map +1 -0
- package/dist/agents/58-payment-flow-tester.d.ts +11 -0
- package/dist/agents/58-payment-flow-tester.d.ts.map +1 -0
- package/dist/agents/58-payment-flow-tester.js +159 -0
- package/dist/agents/58-payment-flow-tester.js.map +1 -0
- package/dist/agents/59-ssl-tls-auditor.d.ts +10 -0
- package/dist/agents/59-ssl-tls-auditor.d.ts.map +1 -0
- package/dist/agents/59-ssl-tls-auditor.js +132 -0
- package/dist/agents/59-ssl-tls-auditor.js.map +1 -0
- package/dist/agents/60-dns-cdn-tester.d.ts +10 -0
- package/dist/agents/60-dns-cdn-tester.d.ts.map +1 -0
- package/dist/agents/60-dns-cdn-tester.js +105 -0
- package/dist/agents/60-dns-cdn-tester.js.map +1 -0
- package/dist/agents/61-docker-health-checker.d.ts +10 -0
- package/dist/agents/61-docker-health-checker.d.ts.map +1 -0
- package/dist/agents/61-docker-health-checker.js +95 -0
- package/dist/agents/61-docker-health-checker.js.map +1 -0
- package/dist/agents/62-env-config-validator.d.ts +10 -0
- package/dist/agents/62-env-config-validator.d.ts.map +1 -0
- package/dist/agents/62-env-config-validator.js +132 -0
- package/dist/agents/62-env-config-validator.js.map +1 -0
- package/dist/agents/63-log-quality-auditor.d.ts +9 -0
- package/dist/agents/63-log-quality-auditor.d.ts.map +1 -0
- package/dist/agents/63-log-quality-auditor.js +121 -0
- package/dist/agents/63-log-quality-auditor.js.map +1 -0
- package/dist/agents/64-analytics-tracker-tester.d.ts +10 -0
- package/dist/agents/64-analytics-tracker-tester.d.ts.map +1 -0
- package/dist/agents/64-analytics-tracker-tester.js +146 -0
- package/dist/agents/64-analytics-tracker-tester.js.map +1 -0
- package/dist/agents/65-gdpr-compliance-tester.d.ts +13 -0
- package/dist/agents/65-gdpr-compliance-tester.d.ts.map +1 -0
- package/dist/agents/65-gdpr-compliance-tester.js +186 -0
- package/dist/agents/65-gdpr-compliance-tester.js.map +1 -0
- package/dist/agents/66-soc2-control-validator.d.ts +14 -0
- package/dist/agents/66-soc2-control-validator.d.ts.map +1 -0
- package/dist/agents/66-soc2-control-validator.js +178 -0
- package/dist/agents/66-soc2-control-validator.js.map +1 -0
- package/dist/agents/67-wcag-aaa-tester.d.ts +13 -0
- package/dist/agents/67-wcag-aaa-tester.d.ts.map +1 -0
- package/dist/agents/67-wcag-aaa-tester.js +207 -0
- package/dist/agents/67-wcag-aaa-tester.js.map +1 -0
- package/dist/agents/68-dead-code-detector.d.ts +9 -0
- package/dist/agents/68-dead-code-detector.d.ts.map +1 -0
- package/dist/agents/68-dead-code-detector.js +116 -0
- package/dist/agents/68-dead-code-detector.js.map +1 -0
- package/dist/agents/69-type-safety-auditor.d.ts +9 -0
- package/dist/agents/69-type-safety-auditor.d.ts.map +1 -0
- package/dist/agents/69-type-safety-auditor.js +148 -0
- package/dist/agents/69-type-safety-auditor.js.map +1 -0
- package/dist/agents/70-complexity-analyzer.d.ts +10 -0
- package/dist/agents/70-complexity-analyzer.d.ts.map +1 -0
- package/dist/agents/70-complexity-analyzer.js +154 -0
- package/dist/agents/70-complexity-analyzer.js.map +1 -0
- package/dist/agents/registry.d.ts +3 -3
- package/dist/agents/registry.d.ts.map +1 -1
- package/dist/agents/registry.js +146 -5
- package/dist/agents/registry.js.map +1 -1
- package/dist/core/license.js +2 -5
- package/dist/core/license.js.map +1 -1
- package/dist/core/orchestrator.d.ts.map +1 -1
- package/dist/core/orchestrator.js +6 -4
- package/dist/core/orchestrator.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Browser, Page } from 'playwright';
|
|
2
|
+
import type { Finding } from '../core/types';
|
|
3
|
+
import { BaseAgent } from './base-agent';
|
|
4
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
5
|
+
import { login } from '../helpers/navigation';
|
|
6
|
+
|
|
7
|
+
/** Routes to check for billing/pricing pages. */
|
|
8
|
+
const BILLING_ROUTES = ['/billing', '/pricing', '/subscription', '/payment', '/plans'] as const;
|
|
9
|
+
|
|
10
|
+
/** Selectors for Stripe checkout elements. */
|
|
11
|
+
const STRIPE_SELECTORS = [
|
|
12
|
+
'iframe[src*="stripe"]',
|
|
13
|
+
'[data-stripe]',
|
|
14
|
+
'#card-element',
|
|
15
|
+
'.StripeElement',
|
|
16
|
+
'button:has-text("Subscribe")',
|
|
17
|
+
'button:has-text("Checkout")',
|
|
18
|
+
'button:has-text("Pay")',
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
export class PaymentFlowTesterAgent extends BaseAgent {
|
|
22
|
+
readonly agentId = 58;
|
|
23
|
+
readonly agentName = 'Payment Flow Tester';
|
|
24
|
+
private baseUrl = '';
|
|
25
|
+
|
|
26
|
+
protected async preFlight(): Promise<void> {
|
|
27
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
28
|
+
this.baseUrl = env.baseUrl;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
protected async execute(): Promise<Finding[]> {
|
|
32
|
+
const findings: Finding[] = [];
|
|
33
|
+
|
|
34
|
+
// Check config for Stripe integration
|
|
35
|
+
const hasStripe = this.config.integrations.some(
|
|
36
|
+
i => i.name.toLowerCase().includes('stripe'),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
if (!hasStripe) {
|
|
40
|
+
findings.push({
|
|
41
|
+
id: `${this.agentId}-no-stripe`,
|
|
42
|
+
type: 'test-bug',
|
|
43
|
+
severity: 'low',
|
|
44
|
+
agentId: this.agentId,
|
|
45
|
+
module: 'payment-flow-tester',
|
|
46
|
+
description: 'No Stripe integration configured — payment flow test skipped',
|
|
47
|
+
});
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rawLoginUrl = this.config.auth.loginUrl ?? '/login';
|
|
52
|
+
const loginUrl = rawLoginUrl.startsWith('http') ? rawLoginUrl : `${this.baseUrl}${rawLoginUrl}`;
|
|
53
|
+
const credentials =
|
|
54
|
+
this.config.auth.credentials?.['admin'] ??
|
|
55
|
+
Object.values(this.config.auth.credentials ?? {})[0];
|
|
56
|
+
|
|
57
|
+
const { chromium } = await import('playwright');
|
|
58
|
+
|
|
59
|
+
let browser: Browser | null = null;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
browser = await chromium.launch({ headless: true });
|
|
63
|
+
const page = await browser.newPage();
|
|
64
|
+
|
|
65
|
+
if (credentials) {
|
|
66
|
+
try {
|
|
67
|
+
await login(page, credentials, loginUrl);
|
|
68
|
+
} catch {
|
|
69
|
+
findings.push({
|
|
70
|
+
id: `${this.agentId}-login-failed`,
|
|
71
|
+
type: 'infra-issue',
|
|
72
|
+
severity: 'medium',
|
|
73
|
+
agentId: this.agentId,
|
|
74
|
+
module: 'payment-flow-tester',
|
|
75
|
+
description: 'Login failed — continuing anonymously',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Try billing routes
|
|
81
|
+
let billingPageFound = false;
|
|
82
|
+
|
|
83
|
+
for (const route of BILLING_ROUTES) {
|
|
84
|
+
const fullUrl = `${this.baseUrl}${route}`;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const response = await page.goto(fullUrl, { waitUntil: 'domcontentloaded', timeout: 10_000 });
|
|
88
|
+
if (response && response.ok()) {
|
|
89
|
+
billingPageFound = true;
|
|
90
|
+
|
|
91
|
+
// Check for Stripe elements
|
|
92
|
+
const stripeFindings = await this.checkStripeElements(page, route);
|
|
93
|
+
findings.push(...stripeFindings);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Route not found — try next
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!billingPageFound) {
|
|
102
|
+
findings.push({
|
|
103
|
+
id: `${this.agentId}-no-billing-page`,
|
|
104
|
+
type: 'test-bug',
|
|
105
|
+
severity: 'low',
|
|
106
|
+
agentId: this.agentId,
|
|
107
|
+
module: 'payment-flow-tester',
|
|
108
|
+
description: `Stripe configured but no billing page found at: ${BILLING_ROUTES.join(', ')}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await page.close().catch(() => undefined);
|
|
113
|
+
} catch (playwrightError) {
|
|
114
|
+
findings.push({
|
|
115
|
+
id: `${this.agentId}-playwright-failure`,
|
|
116
|
+
type: 'infra-issue',
|
|
117
|
+
severity: 'medium',
|
|
118
|
+
agentId: this.agentId,
|
|
119
|
+
module: 'payment-flow-tester',
|
|
120
|
+
description: `Playwright execution failed: ${playwrightError instanceof Error ? playwrightError.message.split('\n')[0] : String(playwrightError)}`,
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
if (browser) await browser.close().catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return findings;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async checkStripeElements(page: Page, route: string): Promise<Finding[]> {
|
|
130
|
+
const findings: Finding[] = [];
|
|
131
|
+
|
|
132
|
+
const selector = STRIPE_SELECTORS.join(', ');
|
|
133
|
+
const stripeCount = await page.locator(selector).count();
|
|
134
|
+
|
|
135
|
+
if (stripeCount === 0) {
|
|
136
|
+
findings.push({
|
|
137
|
+
id: `${this.agentId}-no-stripe-elements`,
|
|
138
|
+
type: 'test-bug',
|
|
139
|
+
severity: 'medium',
|
|
140
|
+
agentId: this.agentId,
|
|
141
|
+
module: 'payment-flow-tester',
|
|
142
|
+
description: `Billing page ${route} found but no Stripe checkout elements detected`,
|
|
143
|
+
});
|
|
144
|
+
return findings;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for Stripe iframe specifically
|
|
148
|
+
const iframeCount = await page.locator('iframe[src*="stripe"]').count();
|
|
149
|
+
if (iframeCount > 0) {
|
|
150
|
+
this.addEvidence({
|
|
151
|
+
type: 'log',
|
|
152
|
+
path: '',
|
|
153
|
+
description: `Stripe iframe detected on ${route}`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Try clicking checkout button
|
|
158
|
+
const checkoutBtn = page.locator('button:has-text("Subscribe"), button:has-text("Checkout"), button:has-text("Pay")');
|
|
159
|
+
if (await checkoutBtn.count() > 0) {
|
|
160
|
+
try {
|
|
161
|
+
await checkoutBtn.first().click();
|
|
162
|
+
await new Promise(resolve => setTimeout(resolve, 3_000));
|
|
163
|
+
|
|
164
|
+
// Check if Stripe iframe loaded after click
|
|
165
|
+
const postClickIframeCount = await page.locator('iframe[src*="stripe"]').count();
|
|
166
|
+
if (postClickIframeCount === 0) {
|
|
167
|
+
findings.push({
|
|
168
|
+
id: `${this.agentId}-stripe-iframe-not-loaded`,
|
|
169
|
+
type: 'test-bug',
|
|
170
|
+
severity: 'medium',
|
|
171
|
+
agentId: this.agentId,
|
|
172
|
+
module: 'payment-flow-tester',
|
|
173
|
+
description: `Checkout button clicked on ${route} but Stripe iframe did not load`,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Click failure — non-critical
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return findings;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Finding } from '../core/types';
|
|
2
|
+
import { BaseAgent } from './base-agent';
|
|
3
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
4
|
+
|
|
5
|
+
export class SslTlsAuditorAgent extends BaseAgent {
|
|
6
|
+
readonly agentId = 59;
|
|
7
|
+
readonly agentName = 'SSL/TLS Auditor';
|
|
8
|
+
private baseUrl = '';
|
|
9
|
+
|
|
10
|
+
protected async preFlight(): Promise<void> {
|
|
11
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
12
|
+
this.baseUrl = env.baseUrl;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected async execute(): Promise<Finding[]> {
|
|
16
|
+
const findings: Finding[] = [];
|
|
17
|
+
|
|
18
|
+
// Check HSTS header
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(this.baseUrl, {
|
|
21
|
+
method: 'HEAD',
|
|
22
|
+
signal: AbortSignal.timeout(10_000),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const hsts = response.headers.get('strict-transport-security');
|
|
26
|
+
|
|
27
|
+
if (!hsts) {
|
|
28
|
+
findings.push({
|
|
29
|
+
id: `${this.agentId}-no-hsts`,
|
|
30
|
+
type: 'code-bug-security',
|
|
31
|
+
severity: 'medium',
|
|
32
|
+
agentId: this.agentId,
|
|
33
|
+
module: 'ssl-tls-auditor',
|
|
34
|
+
description: 'Missing Strict-Transport-Security header',
|
|
35
|
+
suggestedFix: 'Add Strict-Transport-Security header with max-age of at least 31536000',
|
|
36
|
+
});
|
|
37
|
+
} else {
|
|
38
|
+
// Check max-age value
|
|
39
|
+
const maxAgeMatch = hsts.match(/max-age=(\d+)/);
|
|
40
|
+
if (maxAgeMatch) {
|
|
41
|
+
const maxAge = parseInt(maxAgeMatch[1], 10);
|
|
42
|
+
if (maxAge < 31_536_000) {
|
|
43
|
+
findings.push({
|
|
44
|
+
id: `${this.agentId}-short-hsts`,
|
|
45
|
+
type: 'code-bug-security',
|
|
46
|
+
severity: 'low',
|
|
47
|
+
agentId: this.agentId,
|
|
48
|
+
module: 'ssl-tls-auditor',
|
|
49
|
+
description: `HSTS max-age is ${maxAge}s (recommended: 31536000+)`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (fetchError) {
|
|
55
|
+
findings.push({
|
|
56
|
+
id: `${this.agentId}-fetch-failed`,
|
|
57
|
+
type: 'infra-issue',
|
|
58
|
+
severity: 'medium',
|
|
59
|
+
agentId: this.agentId,
|
|
60
|
+
module: 'ssl-tls-auditor',
|
|
61
|
+
description: `Failed to reach ${this.baseUrl}: ${fetchError instanceof Error ? fetchError.message.split('\n')[0] : String(fetchError)}`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check certificate expiry via https module
|
|
66
|
+
if (this.baseUrl.startsWith('https://')) {
|
|
67
|
+
try {
|
|
68
|
+
const https = await import('node:https');
|
|
69
|
+
const url = new URL(this.baseUrl);
|
|
70
|
+
|
|
71
|
+
const certInfo = await new Promise<{ valid_to?: string; valid_from?: string }>((resolve, reject) => {
|
|
72
|
+
const req = https.get(
|
|
73
|
+
{
|
|
74
|
+
hostname: url.hostname,
|
|
75
|
+
port: url.port || 443,
|
|
76
|
+
path: '/',
|
|
77
|
+
rejectUnauthorized: false,
|
|
78
|
+
timeout: 10_000,
|
|
79
|
+
},
|
|
80
|
+
(res) => {
|
|
81
|
+
const socket = res.socket as import('node:tls').TLSSocket;
|
|
82
|
+
const cert = socket.getPeerCertificate?.();
|
|
83
|
+
resolve({ valid_to: cert?.valid_to, valid_from: cert?.valid_from });
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
req.on('error', reject);
|
|
87
|
+
req.on('timeout', () => {
|
|
88
|
+
req.destroy();
|
|
89
|
+
reject(new Error('Certificate check timeout'));
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (certInfo.valid_to) {
|
|
94
|
+
const expiryDate = new Date(certInfo.valid_to);
|
|
95
|
+
const now = new Date();
|
|
96
|
+
const daysUntilExpiry = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
97
|
+
|
|
98
|
+
if (daysUntilExpiry < 0) {
|
|
99
|
+
findings.push({
|
|
100
|
+
id: `${this.agentId}-cert-expired`,
|
|
101
|
+
type: 'code-bug-security',
|
|
102
|
+
severity: 'critical',
|
|
103
|
+
agentId: this.agentId,
|
|
104
|
+
module: 'ssl-tls-auditor',
|
|
105
|
+
description: `SSL certificate expired ${Math.abs(daysUntilExpiry)} days ago`,
|
|
106
|
+
});
|
|
107
|
+
} else if (daysUntilExpiry < 30) {
|
|
108
|
+
findings.push({
|
|
109
|
+
id: `${this.agentId}-cert-expiring-soon`,
|
|
110
|
+
type: 'code-bug-security',
|
|
111
|
+
severity: 'high',
|
|
112
|
+
agentId: this.agentId,
|
|
113
|
+
module: 'ssl-tls-auditor',
|
|
114
|
+
description: `SSL certificate expires in ${daysUntilExpiry} days`,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (certError) {
|
|
119
|
+
findings.push({
|
|
120
|
+
id: `${this.agentId}-cert-check-failed`,
|
|
121
|
+
type: 'infra-issue',
|
|
122
|
+
severity: 'low',
|
|
123
|
+
agentId: this.agentId,
|
|
124
|
+
module: 'ssl-tls-auditor',
|
|
125
|
+
description: `Certificate check failed: ${certError instanceof Error ? certError.message.split('\n')[0] : String(certError)}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
findings.push({
|
|
130
|
+
id: `${this.agentId}-no-https`,
|
|
131
|
+
type: 'code-bug-security',
|
|
132
|
+
severity: 'high',
|
|
133
|
+
agentId: this.agentId,
|
|
134
|
+
module: 'ssl-tls-auditor',
|
|
135
|
+
description: `Base URL ${this.baseUrl} does not use HTTPS`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return findings;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Finding } from '../core/types';
|
|
2
|
+
import { BaseAgent } from './base-agent';
|
|
3
|
+
import { resolveEnvironment } from '../helpers/env-resolver';
|
|
4
|
+
|
|
5
|
+
/** Headers that indicate CDN usage. */
|
|
6
|
+
const CDN_HEADERS: Record<string, string> = {
|
|
7
|
+
'x-cache': 'Generic CDN',
|
|
8
|
+
'cf-ray': 'Cloudflare',
|
|
9
|
+
'x-vercel-cache': 'Vercel',
|
|
10
|
+
'x-cdn': 'Generic CDN',
|
|
11
|
+
'x-amz-cf-id': 'AWS CloudFront',
|
|
12
|
+
'x-served-by': 'Fastly/Varnish',
|
|
13
|
+
'x-azure-ref': 'Azure CDN',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Max acceptable response time in ms. */
|
|
17
|
+
const MAX_RESPONSE_TIME_MS = 2_000;
|
|
18
|
+
|
|
19
|
+
export class DnsCdnTesterAgent extends BaseAgent {
|
|
20
|
+
readonly agentId = 60;
|
|
21
|
+
readonly agentName = 'DNS & CDN Tester';
|
|
22
|
+
private baseUrl = '';
|
|
23
|
+
|
|
24
|
+
protected async preFlight(): Promise<void> {
|
|
25
|
+
const { env } = await resolveEnvironment(this.config, this.phase);
|
|
26
|
+
this.baseUrl = env.baseUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async execute(): Promise<Finding[]> {
|
|
30
|
+
const findings: Finding[] = [];
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
|
|
35
|
+
const response = await fetch(this.baseUrl, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
signal: AbortSignal.timeout(15_000),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const responseTime = Date.now() - startTime;
|
|
41
|
+
|
|
42
|
+
// Check response time
|
|
43
|
+
if (responseTime > MAX_RESPONSE_TIME_MS) {
|
|
44
|
+
findings.push({
|
|
45
|
+
id: `${this.agentId}-slow-response`,
|
|
46
|
+
type: 'infra-issue',
|
|
47
|
+
severity: 'medium',
|
|
48
|
+
agentId: this.agentId,
|
|
49
|
+
module: 'dns-cdn-tester',
|
|
50
|
+
description: `Response time ${responseTime}ms exceeds ${MAX_RESPONSE_TIME_MS}ms threshold`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check CDN headers
|
|
55
|
+
let cdnDetected = false;
|
|
56
|
+
for (const [header, cdnName] of Object.entries(CDN_HEADERS)) {
|
|
57
|
+
const value = response.headers.get(header);
|
|
58
|
+
if (value) {
|
|
59
|
+
cdnDetected = true;
|
|
60
|
+
this.addEvidence({
|
|
61
|
+
type: 'log',
|
|
62
|
+
path: '',
|
|
63
|
+
description: `CDN detected: ${cdnName} (header: ${header}=${value})`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!cdnDetected) {
|
|
69
|
+
findings.push({
|
|
70
|
+
id: `${this.agentId}-no-cdn`,
|
|
71
|
+
type: 'infra-issue',
|
|
72
|
+
severity: 'low',
|
|
73
|
+
agentId: this.agentId,
|
|
74
|
+
module: 'dns-cdn-tester',
|
|
75
|
+
description: 'No CDN detected — consider using a CDN for improved performance',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check cache-control header
|
|
80
|
+
const cacheControl = response.headers.get('cache-control');
|
|
81
|
+
if (!cacheControl) {
|
|
82
|
+
findings.push({
|
|
83
|
+
id: `${this.agentId}-no-cache-control`,
|
|
84
|
+
type: 'infra-issue',
|
|
85
|
+
severity: 'low',
|
|
86
|
+
agentId: this.agentId,
|
|
87
|
+
module: 'dns-cdn-tester',
|
|
88
|
+
description: 'Missing Cache-Control header — caching policy not defined',
|
|
89
|
+
});
|
|
90
|
+
} else if (cacheControl.includes('no-store') || cacheControl.includes('no-cache')) {
|
|
91
|
+
this.addEvidence({
|
|
92
|
+
type: 'log',
|
|
93
|
+
path: '',
|
|
94
|
+
description: `Cache-Control: ${cacheControl} — caching is disabled`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Log response details
|
|
99
|
+
this.addEvidence({
|
|
100
|
+
type: 'log',
|
|
101
|
+
path: '',
|
|
102
|
+
description: `Response time: ${responseTime}ms, Status: ${response.status}`,
|
|
103
|
+
});
|
|
104
|
+
} catch (fetchError) {
|
|
105
|
+
findings.push({
|
|
106
|
+
id: `${this.agentId}-fetch-failed`,
|
|
107
|
+
type: 'infra-issue',
|
|
108
|
+
severity: 'medium',
|
|
109
|
+
agentId: this.agentId,
|
|
110
|
+
module: 'dns-cdn-tester',
|
|
111
|
+
description: `Failed to reach ${this.baseUrl}: ${fetchError instanceof Error ? fetchError.message.split('\n')[0] : String(fetchError)}`,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { Finding } from '../core/types';
|
|
4
|
+
import { BaseAgent } from './base-agent';
|
|
5
|
+
|
|
6
|
+
export class DockerHealthCheckerAgent extends BaseAgent {
|
|
7
|
+
readonly agentId = 61;
|
|
8
|
+
readonly agentName = 'Docker Health Checker';
|
|
9
|
+
|
|
10
|
+
protected async preFlight(): Promise<void> {
|
|
11
|
+
// No network dependencies — file-based agent
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected async execute(): Promise<Finding[]> {
|
|
15
|
+
const findings: Finding[] = [];
|
|
16
|
+
|
|
17
|
+
const projectRoot = this.config.projectRoot ?? process.cwd();
|
|
18
|
+
|
|
19
|
+
// Check Dockerfile
|
|
20
|
+
const dockerfilePath = path.join(projectRoot, 'Dockerfile');
|
|
21
|
+
const dockerfileExists = fs.existsSync(dockerfilePath);
|
|
22
|
+
|
|
23
|
+
if (!dockerfileExists) {
|
|
24
|
+
findings.push({
|
|
25
|
+
id: `${this.agentId}-no-dockerfile`,
|
|
26
|
+
type: 'infra-issue',
|
|
27
|
+
severity: 'low',
|
|
28
|
+
agentId: this.agentId,
|
|
29
|
+
module: 'docker-health-checker',
|
|
30
|
+
description: 'No Dockerfile found in project root',
|
|
31
|
+
});
|
|
32
|
+
return findings;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const dockerfileContent = fs.readFileSync(dockerfilePath, 'utf-8');
|
|
36
|
+
const lines = dockerfileContent.split('\n');
|
|
37
|
+
|
|
38
|
+
// Check 1: USER root at end (security risk)
|
|
39
|
+
const lastUserLine = this.findLastDirective(lines, 'USER');
|
|
40
|
+
if (lastUserLine !== null && lastUserLine.toLowerCase().includes('root')) {
|
|
41
|
+
findings.push({
|
|
42
|
+
id: `${this.agentId}-user-root`,
|
|
43
|
+
type: 'code-bug-security',
|
|
44
|
+
severity: 'high',
|
|
45
|
+
agentId: this.agentId,
|
|
46
|
+
module: 'docker-health-checker',
|
|
47
|
+
file: dockerfilePath,
|
|
48
|
+
description: 'Dockerfile runs as root user — security risk',
|
|
49
|
+
suggestedFix: 'Add a non-root USER directive at the end of the Dockerfile',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check 2: Missing HEALTHCHECK
|
|
54
|
+
const hasHealthcheck = lines.some(line =>
|
|
55
|
+
line.trim().toUpperCase().startsWith('HEALTHCHECK'),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!hasHealthcheck) {
|
|
59
|
+
findings.push({
|
|
60
|
+
id: `${this.agentId}-no-healthcheck`,
|
|
61
|
+
type: 'infra-issue',
|
|
62
|
+
severity: 'medium',
|
|
63
|
+
agentId: this.agentId,
|
|
64
|
+
module: 'docker-health-checker',
|
|
65
|
+
file: dockerfilePath,
|
|
66
|
+
description: 'Dockerfile missing HEALTHCHECK instruction',
|
|
67
|
+
suggestedFix: 'Add HEALTHCHECK CMD curl -f http://localhost:PORT/health || exit 1',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check 3: Missing .dockerignore
|
|
72
|
+
const dockerignorePath = path.join(projectRoot, '.dockerignore');
|
|
73
|
+
if (!fs.existsSync(dockerignorePath)) {
|
|
74
|
+
findings.push({
|
|
75
|
+
id: `${this.agentId}-no-dockerignore`,
|
|
76
|
+
type: 'infra-issue',
|
|
77
|
+
severity: 'low',
|
|
78
|
+
agentId: this.agentId,
|
|
79
|
+
module: 'docker-health-checker',
|
|
80
|
+
description: 'Missing .dockerignore file — build context may include unnecessary files',
|
|
81
|
+
suggestedFix: 'Create .dockerignore to exclude node_modules, .git, .env, etc.',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Log Dockerfile size
|
|
86
|
+
const stats = fs.statSync(dockerfilePath);
|
|
87
|
+
this.addEvidence({
|
|
88
|
+
type: 'log',
|
|
89
|
+
path: dockerfilePath,
|
|
90
|
+
description: `Dockerfile: ${lines.length} lines, ${stats.size} bytes`,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private findLastDirective(lines: string[], directive: string): string | null {
|
|
97
|
+
const upper = directive.toUpperCase();
|
|
98
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
99
|
+
const trimmed = lines[i].trim();
|
|
100
|
+
if (trimmed.toUpperCase().startsWith(upper)) {
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
// Skip empty lines and comments
|
|
104
|
+
if (trimmed.length > 0 && !trimmed.startsWith('#')) {
|
|
105
|
+
// Found a non-matching, non-empty, non-comment line
|
|
106
|
+
// Keep searching
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { Finding } from '../core/types';
|
|
4
|
+
import { BaseAgent } from './base-agent';
|
|
5
|
+
|
|
6
|
+
/** Patterns for URL values. */
|
|
7
|
+
const URL_PATTERN = /^https?:\/\/.+/;
|
|
8
|
+
|
|
9
|
+
/** Patterns for port values. */
|
|
10
|
+
const PORT_PATTERN = /^\d{1,5}$/;
|
|
11
|
+
|
|
12
|
+
/** Common placeholder values that should be replaced. */
|
|
13
|
+
const PLACEHOLDER_PATTERNS = [
|
|
14
|
+
'your-',
|
|
15
|
+
'change-me',
|
|
16
|
+
'TODO',
|
|
17
|
+
'FIXME',
|
|
18
|
+
'xxx',
|
|
19
|
+
'placeholder',
|
|
20
|
+
'example',
|
|
21
|
+
'changeme',
|
|
22
|
+
'replace-',
|
|
23
|
+
'set-this',
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export class EnvConfigValidatorAgent extends BaseAgent {
|
|
27
|
+
readonly agentId = 62;
|
|
28
|
+
readonly agentName = 'Env Config Validator';
|
|
29
|
+
|
|
30
|
+
protected async preFlight(): Promise<void> {
|
|
31
|
+
// File-based agent — no network dependencies
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async execute(): Promise<Finding[]> {
|
|
35
|
+
const findings: Finding[] = [];
|
|
36
|
+
|
|
37
|
+
const projectRoot = this.config.projectRoot ?? process.cwd();
|
|
38
|
+
|
|
39
|
+
const envExamplePath = path.join(projectRoot, '.env.example');
|
|
40
|
+
const envPath = path.join(projectRoot, '.env');
|
|
41
|
+
|
|
42
|
+
const envExampleExists = fs.existsSync(envExamplePath);
|
|
43
|
+
const envExists = fs.existsSync(envPath);
|
|
44
|
+
|
|
45
|
+
if (!envExampleExists && !envExists) {
|
|
46
|
+
findings.push({
|
|
47
|
+
id: `${this.agentId}-no-env-files`,
|
|
48
|
+
type: 'infra-issue',
|
|
49
|
+
severity: 'low',
|
|
50
|
+
agentId: this.agentId,
|
|
51
|
+
module: 'env-config-validator',
|
|
52
|
+
description: 'No .env or .env.example file found in project root',
|
|
53
|
+
});
|
|
54
|
+
return findings;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exampleKeys = envExampleExists ? this.parseEnvFile(envExamplePath) : new Map<string, string>();
|
|
58
|
+
const envKeys = envExists ? this.parseEnvFile(envPath) : new Map<string, string>();
|
|
59
|
+
|
|
60
|
+
// Compare keys: required in example but missing from .env
|
|
61
|
+
if (envExampleExists && envExists) {
|
|
62
|
+
for (const [key] of exampleKeys) {
|
|
63
|
+
if (!envKeys.has(key)) {
|
|
64
|
+
findings.push({
|
|
65
|
+
id: `${this.agentId}-missing-env-var-${key}`,
|
|
66
|
+
type: 'infra-issue',
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
agentId: this.agentId,
|
|
69
|
+
module: 'env-config-validator',
|
|
70
|
+
description: `Required env var ${key} missing from .env (defined in .env.example)`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate .env values format
|
|
77
|
+
if (envExists) {
|
|
78
|
+
for (const [key, value] of envKeys) {
|
|
79
|
+
// Check URLs
|
|
80
|
+
const keyLower = key.toLowerCase();
|
|
81
|
+
if ((keyLower.includes('url') || keyLower.includes('uri') || keyLower.includes('endpoint')) && value.length > 0) {
|
|
82
|
+
if (!URL_PATTERN.test(value)) {
|
|
83
|
+
findings.push({
|
|
84
|
+
id: `${this.agentId}-invalid-url-${key}`,
|
|
85
|
+
type: 'infra-issue',
|
|
86
|
+
severity: 'low',
|
|
87
|
+
agentId: this.agentId,
|
|
88
|
+
module: 'env-config-validator',
|
|
89
|
+
description: `Env var ${key} looks like a URL key but value "${value}" is not a valid URL`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check ports
|
|
95
|
+
if (keyLower.includes('port') && value.length > 0) {
|
|
96
|
+
if (!PORT_PATTERN.test(value)) {
|
|
97
|
+
findings.push({
|
|
98
|
+
id: `${this.agentId}-invalid-port-${key}`,
|
|
99
|
+
type: 'infra-issue',
|
|
100
|
+
severity: 'low',
|
|
101
|
+
agentId: this.agentId,
|
|
102
|
+
module: 'env-config-validator',
|
|
103
|
+
description: `Env var ${key} looks like a port key but value "${value}" is not a valid port number`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for placeholder values
|
|
109
|
+
for (const placeholder of PLACEHOLDER_PATTERNS) {
|
|
110
|
+
if (value.toLowerCase().includes(placeholder.toLowerCase())) {
|
|
111
|
+
findings.push({
|
|
112
|
+
id: `${this.agentId}-placeholder-${key}`,
|
|
113
|
+
type: 'infra-issue',
|
|
114
|
+
severity: 'low',
|
|
115
|
+
agentId: this.agentId,
|
|
116
|
+
module: 'env-config-validator',
|
|
117
|
+
description: `Env var ${key} appears to contain a placeholder value: "${value}"`,
|
|
118
|
+
});
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.addEvidence({
|
|
126
|
+
type: 'log',
|
|
127
|
+
path: '',
|
|
128
|
+
description: `Checked ${exampleKeys.size} example keys, ${envKeys.size} env keys`,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return findings;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private parseEnvFile(filePath: string): Map<string, string> {
|
|
135
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
136
|
+
const result = new Map<string, string>();
|
|
137
|
+
|
|
138
|
+
for (const line of content.split('\n')) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (trimmed.length === 0 || trimmed.startsWith('#')) continue;
|
|
141
|
+
|
|
142
|
+
const eqIndex = trimmed.indexOf('=');
|
|
143
|
+
if (eqIndex === -1) continue;
|
|
144
|
+
|
|
145
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
146
|
+
const value = trimmed.substring(eqIndex + 1).trim().replace(/^["']|["']$/g, '');
|
|
147
|
+
result.set(key, value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|