@arcadialdev/arcality 2.4.24 → 2.4.26
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/.agents/skills/e2e-testing-expert/SKILL.md +28 -28
- package/.agents/skills/security-qa/SKILL.md +254 -0
- package/README.md +95 -95
- package/bin/arcality.mjs +55 -55
- package/package.json +68 -68
- package/playwright.config.js +33 -33
- package/scripts/gen-and-run.mjs +932 -929
- package/scripts/init.mjs +297 -297
- package/scripts/postinstall.mjs +77 -77
- package/src/arcalityClient.mjs +266 -266
- package/src/configLoader.mjs +179 -179
- package/src/configManager.mjs +172 -172
- package/src/envSetup.ts +205 -205
- package/src/index.ts +25 -25
- package/src/services/collectiveMemoryService.ts +1 -1
- package/src/services/securityScanner.ts +128 -0
- package/tests/_helpers/ArcalityReporter.js +706 -303
- package/tests/_helpers/ArcalityReporter.ts +262 -27
- package/tests/_helpers/agentic-runner.bundle.spec.js +357 -5
- package/tests/_helpers/agentic-runner.spec.ts +37 -1
- package/tests/_helpers/ai-agent-helper.ts +128 -6
- package/tests/_helpers/qa-security-tools.ts +265 -0
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
import { Page, TestInfo } from '@playwright/test';
|
|
2
2
|
import { qaAdvancedTools } from './qa-tools';
|
|
3
|
+
import {
|
|
4
|
+
test_xss_injection,
|
|
5
|
+
test_auth_bypass,
|
|
6
|
+
audit_http_headers,
|
|
7
|
+
scan_sensitive_data_exposure,
|
|
8
|
+
SecurityFinding
|
|
9
|
+
} from './qa-security-tools';
|
|
10
|
+
|
|
11
|
+
const qaSecurityTools = [
|
|
12
|
+
{
|
|
13
|
+
name: "test_xss_injection",
|
|
14
|
+
description: "Injects a standard XSS payload into an input field to test for reflected XSS vulnerabilities. Reports a vulnerability if an alert is triggered.",
|
|
15
|
+
parameters: {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
selector: { type: "string", description: "The CSS selector of the input field to test." }
|
|
19
|
+
},
|
|
20
|
+
required: ["selector"]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "test_auth_bypass",
|
|
25
|
+
description: "Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
url: { type: "string", description: "The protected URL to test." },
|
|
30
|
+
redirectUrl: { type: "string", description: "The expected URL to be redirected to if unauthorized (e.g., '/login')." }
|
|
31
|
+
},
|
|
32
|
+
required: ["url", "redirectUrl"]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "audit_http_headers",
|
|
37
|
+
description: "Audits the current page's HTTP response headers for common security headers like CSP, HSTS, etc.",
|
|
38
|
+
parameters: { type: "object", properties: {} }
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "scan_sensitive_data_exposure",
|
|
42
|
+
description: "Scans localStorage and sessionStorage for keys that might contain sensitive data (e.g., 'token', 'password').",
|
|
43
|
+
parameters: { type: "object", properties: {} }
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
import { SecurityScanner } from '../../src/services/securityScanner';
|
|
3
47
|
import chalk from 'chalk';
|
|
4
48
|
import * as fs from 'fs';
|
|
5
49
|
import * as path from 'path';
|
|
@@ -478,8 +522,12 @@ ${best.steps.slice(0, 15).map((s: any) => ` - ${s}`).join('\n')}
|
|
|
478
522
|
const memStep = m.steps_data[historyLength];
|
|
479
523
|
if (!memStep) return false;
|
|
480
524
|
|
|
481
|
-
const
|
|
482
|
-
|
|
525
|
+
const normalizeUrl = (u: string) =>
|
|
526
|
+
u.replace(/\/[a-z]{2}-[A-Z]{2}\//g, '/{locale}/')
|
|
527
|
+
.replace(/\/[a-z]{2}\//g, '/{lang}/');
|
|
528
|
+
|
|
529
|
+
const memUrl = normalizeUrl((memStep.url || '').split('?')[0]);
|
|
530
|
+
const currUrl = normalizeUrl(currentUrl.split('?')[0]);
|
|
483
531
|
|
|
484
532
|
// URL Validation
|
|
485
533
|
if (memUrl !== currUrl) return false;
|
|
@@ -539,7 +587,7 @@ ${best.steps.slice(0, 15).map((s: any) => ` - ${s}`).join('\n')}
|
|
|
539
587
|
return null;
|
|
540
588
|
}
|
|
541
589
|
|
|
542
|
-
async askIA(prompt: string, history: string[] = []): Promise<AgentAction> {
|
|
590
|
+
async askIA(prompt: string, history: string[] = [], stepCount?: number): Promise<AgentAction> {
|
|
543
591
|
// El modo proxy (ARCALITY_API_URL) está integrado en el bucle de fetch de abajo.
|
|
544
592
|
// Si ARCALITY_API_URL está definido, se enruta automáticamente a /api/v1/ai/proxy.
|
|
545
593
|
// Si no, requiere ANTHROPIC_API_KEY para llamada directa.
|
|
@@ -632,6 +680,11 @@ ${best.steps.slice(0, 15).map((s: any) => ` - ${s}`).join('\n')}
|
|
|
632
680
|
name: t.name,
|
|
633
681
|
description: t.description,
|
|
634
682
|
parameters: (t as any).input_schema || (t as any).parameters
|
|
683
|
+
})),
|
|
684
|
+
...qaSecurityTools.map(t => ({
|
|
685
|
+
name: t.name,
|
|
686
|
+
description: t.description,
|
|
687
|
+
parameters: (t as any).input_schema || (t as any).parameters
|
|
635
688
|
}))
|
|
636
689
|
];
|
|
637
690
|
|
|
@@ -828,8 +881,8 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
828
881
|
? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy`
|
|
829
882
|
: "https://api.anthropic.com/v1/messages";
|
|
830
883
|
|
|
831
|
-
|
|
832
|
-
console.log(`>>ARCALITY_STATUS>> 📡 Llamando a: ${endpointUrl} (turno ${turn
|
|
884
|
+
const displayTurn = stepCount !== undefined ? stepCount : (turn + 1);
|
|
885
|
+
console.log(`>>ARCALITY_STATUS>> 📡 Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, reintento ${turn}` : ''})`);
|
|
833
886
|
|
|
834
887
|
const headers: Record<string, string> = {
|
|
835
888
|
"Content-Type": "application/json"
|
|
@@ -837,6 +890,8 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
837
890
|
|
|
838
891
|
if (isProxyMode) {
|
|
839
892
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
893
|
+
const missionId = process.env.ARCALITY_MISSION_ID || '';
|
|
894
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
840
895
|
} else {
|
|
841
896
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
842
897
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -1162,7 +1217,68 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
1162
1217
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
|
|
1163
1218
|
}
|
|
1164
1219
|
}
|
|
1165
|
-
|
|
1220
|
+
} else if (toolName === "test_xss_injection") {
|
|
1221
|
+
const { idx, payload_type } = toolInput;
|
|
1222
|
+
const el = state.components.find(c => c.idx === idx);
|
|
1223
|
+
if (!el) {
|
|
1224
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
|
|
1225
|
+
} else {
|
|
1226
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ [Security] Probando inyección XSS en IDX ${idx}...`);
|
|
1227
|
+
let payload = "<script>alert(1)</script>";
|
|
1228
|
+
if (payload_type === 'image') payload = "<img src=x onerror=alert(1)>";
|
|
1229
|
+
if (payload_type === 'svg') payload = "<svg onload=alert(1)>";
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
const locator = this.page.locator(`xpath=${el.selector}`).first();
|
|
1233
|
+
await locator.fill(payload);
|
|
1234
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Asegúrate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regresó renderizado tal cual o sanitizado.` });
|
|
1235
|
+
if (this.testInfo) this.testInfo.attach('security_tool_call', { body: JSON.stringify({ action: "test_xss_injection", target: idx, payload }), contentType: "application/json" });
|
|
1236
|
+
} catch (e: any) {
|
|
1237
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error injecting payload: ${e.message}` });
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
} else if (toolName === "test_auth_bypass") {
|
|
1241
|
+
const { target_url } = toolInput;
|
|
1242
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ [Security] Intentando Auth Bypass en ${target_url}...`);
|
|
1243
|
+
try {
|
|
1244
|
+
const baseUrl = new URL(this.page.url()).origin;
|
|
1245
|
+
const fullUrl = new URL(target_url, baseUrl).toString();
|
|
1246
|
+
await this.page.goto(fullUrl);
|
|
1247
|
+
await this.page.waitForLoadState('networkidle', { timeout: 3000 }).catch(() => {});
|
|
1248
|
+
const resultingUrl = this.page.url();
|
|
1249
|
+
let resultData = `Navegado a ${fullUrl}. \nURL resultante: ${resultingUrl}. \n`;
|
|
1250
|
+
if (resultingUrl !== fullUrl) resultData += `Posiblemente redirigido (el bypass falló o te enviaron a Login).`;
|
|
1251
|
+
else resultData += `URL se mantuvo igual. ¡Esto podría ser un IDOR o Auth Bypass si ves datos a los que no deberías acceder!`;
|
|
1252
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: resultData });
|
|
1253
|
+
} catch (e: any) {
|
|
1254
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error test_auth_bypass: ${e.message}` });
|
|
1255
|
+
}
|
|
1256
|
+
} else if (toolName === "scan_sensitive_data_exposure") {
|
|
1257
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ [Security] Escaneando datos sensibles...`);
|
|
1258
|
+
try {
|
|
1259
|
+
const issues = await scan_sensitive_data_exposure(this.page);
|
|
1260
|
+
if (issues.length > 0) {
|
|
1261
|
+
const report = issues.map((i: any) => `- ${i.category}: ${i.description}\n Evidence: ${JSON.stringify(i.evidence)}`).join('\n');
|
|
1262
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades detectadas:\n${report}` });
|
|
1263
|
+
} else {
|
|
1264
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron datos sensibles en LocalStorage/SessionStorage en este chequeo." });
|
|
1265
|
+
}
|
|
1266
|
+
} catch (e: any) {
|
|
1267
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scan_sensitive_data_exposure: ${e.message}` });
|
|
1268
|
+
}
|
|
1269
|
+
} else if (toolName === "audit_http_headers") {
|
|
1270
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ [Security] Auditando headers HTTP...`);
|
|
1271
|
+
try {
|
|
1272
|
+
const issues = await audit_http_headers(this.page);
|
|
1273
|
+
if (issues.length > 0) {
|
|
1274
|
+
const report = issues.map((i: any) => `- ${i.category}: ${i.description}`).join('\n');
|
|
1275
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades de Headers detectadas:\n${report}` });
|
|
1276
|
+
} else {
|
|
1277
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad básicos." });
|
|
1278
|
+
}
|
|
1279
|
+
} catch (e: any) {
|
|
1280
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error audit_http_headers: ${e.message}` });
|
|
1281
|
+
}
|
|
1166
1282
|
} else if (toolName === "create_test_evidence") {
|
|
1167
1283
|
const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
|
|
1168
1284
|
console.log(`📸 [SKILL] Creating test evidence: "${save_as}"...`);
|
|
@@ -1385,6 +1501,8 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
1385
1501
|
} catch (e: any) {
|
|
1386
1502
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
|
|
1387
1503
|
}
|
|
1504
|
+
} else {
|
|
1505
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error: Tool '${toolName}' not recognized by this version.` });
|
|
1388
1506
|
}
|
|
1389
1507
|
}
|
|
1390
1508
|
|
|
@@ -1438,6 +1556,8 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
1438
1556
|
|
|
1439
1557
|
if (isProxyMode) {
|
|
1440
1558
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1559
|
+
const missionId = process.env.ARCALITY_MISSION_ID || '';
|
|
1560
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1441
1561
|
} else {
|
|
1442
1562
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
1443
1563
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -1487,6 +1607,8 @@ ${history.slice(-25).join('\n') || 'None'}`
|
|
|
1487
1607
|
|
|
1488
1608
|
if (isProxyMode) {
|
|
1489
1609
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1610
|
+
const missionId = process.env.ARCALITY_MISSION_ID || '';
|
|
1611
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1490
1612
|
} else {
|
|
1491
1613
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY!;
|
|
1492
1614
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { Page, expect } from '@playwright/test';
|
|
3
|
+
|
|
4
|
+
export type Severity = 'Critical' | 'High' | 'Medium' | 'Low' | 'Info';
|
|
5
|
+
|
|
6
|
+
export type SecurityFindingType = 'confirmed_vulnerability' | 'security_misconfiguration' | 'potential_issue' | 'informational';
|
|
7
|
+
export type Confidence = 'High' | 'Medium' | 'Low';
|
|
8
|
+
export type Exploitability = 'confirmed' | 'likely' | 'unknown' | 'not_confirmed';
|
|
9
|
+
|
|
10
|
+
export interface SecurityFinding {
|
|
11
|
+
type: SecurityFindingType;
|
|
12
|
+
category: string;
|
|
13
|
+
subcategory?: string;
|
|
14
|
+
severity: Severity;
|
|
15
|
+
confidence: Confidence;
|
|
16
|
+
exploitability: Exploitability;
|
|
17
|
+
status: 'detected';
|
|
18
|
+
description: string;
|
|
19
|
+
impact?: string;
|
|
20
|
+
evidence: {
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
};
|
|
23
|
+
reproductionSteps?: string[];
|
|
24
|
+
remediation: string;
|
|
25
|
+
classification?: {
|
|
26
|
+
owaspTop10?: string;
|
|
27
|
+
cwe?: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Attempts to inject a simple XSS payload into a specified element and checks for an alert.
|
|
33
|
+
* @param page - The Playwright Page object.
|
|
34
|
+
* @param selector - The selector for the input field to test.
|
|
35
|
+
* @returns A vulnerability object if successful, null otherwise.
|
|
36
|
+
*/
|
|
37
|
+
export async function test_xss_injection(
|
|
38
|
+
page: Page,
|
|
39
|
+
selector: string,
|
|
40
|
+
): Promise<SecurityFinding | null> {
|
|
41
|
+
const payload = `<script>alert('XSS-ARCALITY-TEST')</script>`;
|
|
42
|
+
console.log(`[QA-SEC] Testing XSS on selector: ${selector}`);
|
|
43
|
+
|
|
44
|
+
let alertTriggered = false;
|
|
45
|
+
page.once('dialog', async (dialog) => {
|
|
46
|
+
if (dialog.message().includes('XSS-ARCALITY-TEST')) {
|
|
47
|
+
alertTriggered = true;
|
|
48
|
+
}
|
|
49
|
+
await dialog.dismiss();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await page.fill(selector, payload);
|
|
53
|
+
// Submit the form or trigger the action that would process the input
|
|
54
|
+
// This part is crucial and might need to be adapted based on the application's behavior
|
|
55
|
+
await page.press(selector, 'Enter');
|
|
56
|
+
|
|
57
|
+
// Wait a moment for any potential script execution
|
|
58
|
+
await page.waitForTimeout(1000);
|
|
59
|
+
|
|
60
|
+
if (alertTriggered) {
|
|
61
|
+
console.log(`[QA-SEC] XSS Vulnerability DETECTED at ${selector}`);
|
|
62
|
+
return {
|
|
63
|
+
type: 'confirmed_vulnerability',
|
|
64
|
+
category: 'Injection',
|
|
65
|
+
subcategory: 'Reflected XSS',
|
|
66
|
+
severity: 'High',
|
|
67
|
+
confidence: 'High',
|
|
68
|
+
exploitability: 'confirmed',
|
|
69
|
+
status: 'detected',
|
|
70
|
+
description: `A reflected Cross-Site Scripting (XSS) vulnerability was detected in the element '${selector}'. The application executed a script injected into this input field.`,
|
|
71
|
+
impact: 'Allows execution of arbitrary JavaScript in the context of the user session, potentially leading to session hijacking, credential theft, or unauthorized actions.',
|
|
72
|
+
evidence: {
|
|
73
|
+
payload,
|
|
74
|
+
url: page.url(),
|
|
75
|
+
selector,
|
|
76
|
+
},
|
|
77
|
+
reproductionSteps: [
|
|
78
|
+
`Navigate to the page containing '${selector}'.`,
|
|
79
|
+
`Inject the payload: ${payload}`,
|
|
80
|
+
'Trigger the form submission or blur event.',
|
|
81
|
+
'Observe the script execution (alert dialog).'
|
|
82
|
+
],
|
|
83
|
+
remediation: 'Sanitize all user-supplied input on the server-side before rendering it back to the page. Use libraries like DOMPurify on the client-side as an additional layer of defense.',
|
|
84
|
+
classification: {
|
|
85
|
+
owaspTop10: 'A03:2021 Injection',
|
|
86
|
+
cwe: 'CWE-79'
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log(`[QA-SEC] No XSS detected for selector: ${selector}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Checks if a given URL, assumed to be protected, can be accessed without authentication.
|
|
97
|
+
* @param page - The Playwright Page object.
|
|
98
|
+
* @param url - The protected URL to test.
|
|
99
|
+
* @param redirectUrl - The URL the app should redirect to if unauthorized.
|
|
100
|
+
* @returns A vulnerability object if bypass is successful, null otherwise.
|
|
101
|
+
*/
|
|
102
|
+
export async function test_auth_bypass(
|
|
103
|
+
page: Page,
|
|
104
|
+
url: string,
|
|
105
|
+
redirectUrl: string | RegExp,
|
|
106
|
+
): Promise<SecurityFinding | null> {
|
|
107
|
+
console.log(`[QA-SEC] Testing Auth Bypass on URL: ${url}`);
|
|
108
|
+
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
109
|
+
|
|
110
|
+
// If the page loads the protected content (doesn't redirect or redirects to a different page)
|
|
111
|
+
if (response?.ok() && !page.url().match(redirectUrl)) {
|
|
112
|
+
console.log(`[QA-SEC] Auth Bypass DETECTED for URL: ${url}`);
|
|
113
|
+
return {
|
|
114
|
+
type: 'confirmed_vulnerability',
|
|
115
|
+
category: 'Broken Access Control',
|
|
116
|
+
subcategory: 'Authentication Bypass',
|
|
117
|
+
severity: 'Critical',
|
|
118
|
+
confidence: 'High',
|
|
119
|
+
exploitability: 'confirmed',
|
|
120
|
+
status: 'detected',
|
|
121
|
+
description: `Unauthorized access to the protected route '${url}' was successful. The application failed to enforce authentication, allowing a direct bypass to sensitive content.`,
|
|
122
|
+
impact: 'An unauthenticated attacker can access protected resources, potentially reading or modifying sensitive user data.',
|
|
123
|
+
evidence: {
|
|
124
|
+
accessedUrl: url,
|
|
125
|
+
finalUrl: page.url(),
|
|
126
|
+
},
|
|
127
|
+
reproductionSteps: [
|
|
128
|
+
'Ensure no active session (cleared cookies/storage).',
|
|
129
|
+
`Navigate directly to: ${url}`,
|
|
130
|
+
'Observe that the protected page loads without redirection.'
|
|
131
|
+
],
|
|
132
|
+
remediation: 'Implement server-side middleware to verify user authentication and authorization status before serving any protected routes.',
|
|
133
|
+
classification: {
|
|
134
|
+
owaspTop10: 'A01:2021 Broken Access Control',
|
|
135
|
+
cwe: 'CWE-285'
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(`[QA-SEC] No Auth Bypass detected for URL: ${url}`);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Audits the HTTP security headers of the current page.
|
|
146
|
+
* @param page - The Playwright Page object.
|
|
147
|
+
* @returns An array of vulnerability objects for missing headers.
|
|
148
|
+
*/
|
|
149
|
+
export async function audit_http_headers(page: Page): Promise<SecurityFinding[]> {
|
|
150
|
+
console.log(`[QA-SEC] Auditing HTTP Headers for: ${page.url()}`);
|
|
151
|
+
const response = await page.reload({ waitUntil: 'domcontentloaded' });
|
|
152
|
+
const headers = response?.headers();
|
|
153
|
+
const vulnerabilities: SecurityFinding[] = [];
|
|
154
|
+
|
|
155
|
+
const securityHeaders = {
|
|
156
|
+
'Content-Security-Policy': {
|
|
157
|
+
severity: 'Medium',
|
|
158
|
+
remediation: 'Implement a strict Content-Security-Policy (CSP) to mitigate XSS and other injection attacks. Start with a restrictive policy and gradually allow trusted sources.',
|
|
159
|
+
impact: 'If an injection issue exists elsewhere in the application, the lack of CSP may increase exploitability and impact.',
|
|
160
|
+
cwe: 'CWE-16'
|
|
161
|
+
},
|
|
162
|
+
'Strict-Transport-Security': {
|
|
163
|
+
severity: 'Medium',
|
|
164
|
+
remediation: 'Implement HTTP Strict Transport Security (HSTS) to force browsers to communicate over HTTPS, preventing downgrade attacks.',
|
|
165
|
+
impact: 'Man-in-the-middle attacks could downgrade the connection to HTTP, allowing traffic interception.',
|
|
166
|
+
cwe: 'CWE-319'
|
|
167
|
+
},
|
|
168
|
+
'X-Content-Type-Options': {
|
|
169
|
+
severity: 'Low',
|
|
170
|
+
remediation: 'Set `X-Content-Type-Options: nosniff` to prevent browsers from MIME-sniffing a response away from the declared content-type.',
|
|
171
|
+
impact: 'Browsers may interpret non-executable MIME types as executable, leading to potential XSS.',
|
|
172
|
+
cwe: 'CWE-430'
|
|
173
|
+
},
|
|
174
|
+
'X-Frame-Options': {
|
|
175
|
+
severity: 'Medium',
|
|
176
|
+
remediation: 'Set `X-Frame-Options: SAMEORIGIN` or `DENY` to protect against clickjacking attacks by preventing the page from being loaded in an iframe on other domains.',
|
|
177
|
+
impact: 'Attackers could trick users into clicking visually hidden elements, causing unintended actions (Clickjacking).',
|
|
178
|
+
cwe: 'CWE-1021'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
for (const [header, info] of Object.entries(securityHeaders)) {
|
|
183
|
+
if (!headers || !Object.keys(headers).find(h => h.toLowerCase() === header.toLowerCase())) {
|
|
184
|
+
vulnerabilities.push({
|
|
185
|
+
type: 'security_misconfiguration',
|
|
186
|
+
category: 'Missing Security Header',
|
|
187
|
+
subcategory: header,
|
|
188
|
+
severity: info.severity as Severity,
|
|
189
|
+
confidence: 'High',
|
|
190
|
+
exploitability: 'not_confirmed',
|
|
191
|
+
status: 'detected',
|
|
192
|
+
description: `The '${header}' HTTP header is missing. This header is crucial for defending against various web attacks.`,
|
|
193
|
+
impact: info.impact,
|
|
194
|
+
evidence: {
|
|
195
|
+
url: page.url(),
|
|
196
|
+
missingHeader: header
|
|
197
|
+
},
|
|
198
|
+
reproductionSteps: [
|
|
199
|
+
`Navigate to ${page.url()}`,
|
|
200
|
+
`Inspect the response headers.`,
|
|
201
|
+
`Verify that ${header} is absent.`
|
|
202
|
+
],
|
|
203
|
+
remediation: info.remediation,
|
|
204
|
+
classification: {
|
|
205
|
+
owaspTop10: 'A05:2021 Security Misconfiguration',
|
|
206
|
+
cwe: info.cwe
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} missing security headers.`);
|
|
213
|
+
return vulnerabilities;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Scans localStorage and sessionStorage for potentially sensitive data.
|
|
218
|
+
* @param page - The Playwright Page object.
|
|
219
|
+
* @returns An array of vulnerability objects for any sensitive data found.
|
|
220
|
+
*/
|
|
221
|
+
export async function scan_sensitive_data_exposure(page: Page): Promise<SecurityFinding[]> {
|
|
222
|
+
console.log(`[QA-SEC] Scanning localStorage and sessionStorage for sensitive data.`);
|
|
223
|
+
const vulnerabilities: SecurityFinding[] = [];
|
|
224
|
+
const sensitiveKeywords = ['token', 'password', 'secret', 'key', 'jwt', 'auth'];
|
|
225
|
+
|
|
226
|
+
const storages = {
|
|
227
|
+
localStorage: await page.evaluate(() => Object.entries(window.localStorage)),
|
|
228
|
+
sessionStorage: await page.evaluate(() => Object.entries(window.sessionStorage)),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const [storageType, items] of Object.entries(storages)) {
|
|
232
|
+
for (const [key, value] of items) {
|
|
233
|
+
if (sensitiveKeywords.some(keyword => key.toLowerCase().includes(keyword))) {
|
|
234
|
+
vulnerabilities.push({
|
|
235
|
+
type: 'potential_issue',
|
|
236
|
+
category: 'Sensitive Data Exposure',
|
|
237
|
+
subcategory: 'Browser Storage',
|
|
238
|
+
severity: 'Medium',
|
|
239
|
+
confidence: 'Medium',
|
|
240
|
+
exploitability: 'unknown',
|
|
241
|
+
status: 'detected',
|
|
242
|
+
description: `Potentially sensitive data was found in ${storageType}. Storing sensitive information like tokens or keys in browser storage is insecure.`,
|
|
243
|
+
impact: 'If an attacker discovers an XSS vulnerability, they can read localStorage/sessionStorage and exfiltrate secrets.',
|
|
244
|
+
evidence: {
|
|
245
|
+
storage: storageType,
|
|
246
|
+
key: key,
|
|
247
|
+
valuePreview: value ? `${value.substring(0, 10)}...` : '(empty)',
|
|
248
|
+
},
|
|
249
|
+
reproductionSteps: [
|
|
250
|
+
`Open Developer Tools on ${page.url()}`,
|
|
251
|
+
`Check ${storageType} for the key '${key}'.`
|
|
252
|
+
],
|
|
253
|
+
remediation: 'Avoid storing sensitive data in browser storage. Use secure, HttpOnly cookies for session tokens. If you must store data, encrypt it first.',
|
|
254
|
+
classification: {
|
|
255
|
+
owaspTop10: 'A04:2021 Insecure Design',
|
|
256
|
+
cwe: 'CWE-312'
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} instances of sensitive data in browser storage.`);
|
|
264
|
+
return vulnerabilities;
|
|
265
|
+
}
|