@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
|
@@ -303,6 +303,113 @@ var qaAdvancedTools = [
|
|
|
303
303
|
scrollPageTool
|
|
304
304
|
];
|
|
305
305
|
|
|
306
|
+
// tests/_helpers/qa-security-tools.ts
|
|
307
|
+
async function audit_http_headers(page) {
|
|
308
|
+
console.log(`[QA-SEC] Auditing HTTP Headers for: ${page.url()}`);
|
|
309
|
+
const response = await page.reload({ waitUntil: "domcontentloaded" });
|
|
310
|
+
const headers = response?.headers();
|
|
311
|
+
const vulnerabilities = [];
|
|
312
|
+
const securityHeaders = {
|
|
313
|
+
"Content-Security-Policy": {
|
|
314
|
+
severity: "Medium",
|
|
315
|
+
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.",
|
|
316
|
+
impact: "If an injection issue exists elsewhere in the application, the lack of CSP may increase exploitability and impact.",
|
|
317
|
+
cwe: "CWE-16"
|
|
318
|
+
},
|
|
319
|
+
"Strict-Transport-Security": {
|
|
320
|
+
severity: "Medium",
|
|
321
|
+
remediation: "Implement HTTP Strict Transport Security (HSTS) to force browsers to communicate over HTTPS, preventing downgrade attacks.",
|
|
322
|
+
impact: "Man-in-the-middle attacks could downgrade the connection to HTTP, allowing traffic interception.",
|
|
323
|
+
cwe: "CWE-319"
|
|
324
|
+
},
|
|
325
|
+
"X-Content-Type-Options": {
|
|
326
|
+
severity: "Low",
|
|
327
|
+
remediation: "Set `X-Content-Type-Options: nosniff` to prevent browsers from MIME-sniffing a response away from the declared content-type.",
|
|
328
|
+
impact: "Browsers may interpret non-executable MIME types as executable, leading to potential XSS.",
|
|
329
|
+
cwe: "CWE-430"
|
|
330
|
+
},
|
|
331
|
+
"X-Frame-Options": {
|
|
332
|
+
severity: "Medium",
|
|
333
|
+
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.",
|
|
334
|
+
impact: "Attackers could trick users into clicking visually hidden elements, causing unintended actions (Clickjacking).",
|
|
335
|
+
cwe: "CWE-1021"
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
for (const [header, info] of Object.entries(securityHeaders)) {
|
|
339
|
+
if (!headers || !Object.keys(headers).find((h) => h.toLowerCase() === header.toLowerCase())) {
|
|
340
|
+
vulnerabilities.push({
|
|
341
|
+
type: "security_misconfiguration",
|
|
342
|
+
category: "Missing Security Header",
|
|
343
|
+
subcategory: header,
|
|
344
|
+
severity: info.severity,
|
|
345
|
+
confidence: "High",
|
|
346
|
+
exploitability: "not_confirmed",
|
|
347
|
+
status: "detected",
|
|
348
|
+
description: `The '${header}' HTTP header is missing. This header is crucial for defending against various web attacks.`,
|
|
349
|
+
impact: info.impact,
|
|
350
|
+
evidence: {
|
|
351
|
+
url: page.url(),
|
|
352
|
+
missingHeader: header
|
|
353
|
+
},
|
|
354
|
+
reproductionSteps: [
|
|
355
|
+
`Navigate to ${page.url()}`,
|
|
356
|
+
`Inspect the response headers.`,
|
|
357
|
+
`Verify that ${header} is absent.`
|
|
358
|
+
],
|
|
359
|
+
remediation: info.remediation,
|
|
360
|
+
classification: {
|
|
361
|
+
owaspTop10: "A05:2021 Security Misconfiguration",
|
|
362
|
+
cwe: info.cwe
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} missing security headers.`);
|
|
368
|
+
return vulnerabilities;
|
|
369
|
+
}
|
|
370
|
+
async function scan_sensitive_data_exposure(page) {
|
|
371
|
+
console.log(`[QA-SEC] Scanning localStorage and sessionStorage for sensitive data.`);
|
|
372
|
+
const vulnerabilities = [];
|
|
373
|
+
const sensitiveKeywords = ["token", "password", "secret", "key", "jwt", "auth"];
|
|
374
|
+
const storages = {
|
|
375
|
+
localStorage: await page.evaluate(() => Object.entries(window.localStorage)),
|
|
376
|
+
sessionStorage: await page.evaluate(() => Object.entries(window.sessionStorage))
|
|
377
|
+
};
|
|
378
|
+
for (const [storageType, items] of Object.entries(storages)) {
|
|
379
|
+
for (const [key, value] of items) {
|
|
380
|
+
if (sensitiveKeywords.some((keyword) => key.toLowerCase().includes(keyword))) {
|
|
381
|
+
vulnerabilities.push({
|
|
382
|
+
type: "potential_issue",
|
|
383
|
+
category: "Sensitive Data Exposure",
|
|
384
|
+
subcategory: "Browser Storage",
|
|
385
|
+
severity: "Medium",
|
|
386
|
+
confidence: "Medium",
|
|
387
|
+
exploitability: "unknown",
|
|
388
|
+
status: "detected",
|
|
389
|
+
description: `Potentially sensitive data was found in ${storageType}. Storing sensitive information like tokens or keys in browser storage is insecure.`,
|
|
390
|
+
impact: "If an attacker discovers an XSS vulnerability, they can read localStorage/sessionStorage and exfiltrate secrets.",
|
|
391
|
+
evidence: {
|
|
392
|
+
storage: storageType,
|
|
393
|
+
key,
|
|
394
|
+
valuePreview: value ? `${value.substring(0, 10)}...` : "(empty)"
|
|
395
|
+
},
|
|
396
|
+
reproductionSteps: [
|
|
397
|
+
`Open Developer Tools on ${page.url()}`,
|
|
398
|
+
`Check ${storageType} for the key '${key}'.`
|
|
399
|
+
],
|
|
400
|
+
remediation: "Avoid storing sensitive data in browser storage. Use secure, HttpOnly cookies for session tokens. If you must store data, encrypt it first.",
|
|
401
|
+
classification: {
|
|
402
|
+
owaspTop10: "A04:2021 Insecure Design",
|
|
403
|
+
cwe: "CWE-312"
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
console.log(`[QA-SEC] Found ${vulnerabilities.length} instances of sensitive data in browser storage.`);
|
|
410
|
+
return vulnerabilities;
|
|
411
|
+
}
|
|
412
|
+
|
|
306
413
|
// tests/_helpers/ai-agent-helper.ts
|
|
307
414
|
var fs = __toESM(require("fs"));
|
|
308
415
|
var path = __toESM(require("path"));
|
|
@@ -637,6 +744,41 @@ async function getKnownFieldIdentifiers(pathUrl) {
|
|
|
637
744
|
}
|
|
638
745
|
|
|
639
746
|
// tests/_helpers/ai-agent-helper.ts
|
|
747
|
+
var qaSecurityTools = [
|
|
748
|
+
{
|
|
749
|
+
name: "test_xss_injection",
|
|
750
|
+
description: "Injects a standard XSS payload into an input field to test for reflected XSS vulnerabilities. Reports a vulnerability if an alert is triggered.",
|
|
751
|
+
parameters: {
|
|
752
|
+
type: "object",
|
|
753
|
+
properties: {
|
|
754
|
+
selector: { type: "string", description: "The CSS selector of the input field to test." }
|
|
755
|
+
},
|
|
756
|
+
required: ["selector"]
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "test_auth_bypass",
|
|
761
|
+
description: "Attempts to directly navigate to a URL that should be protected to check for authentication bypass vulnerabilities.",
|
|
762
|
+
parameters: {
|
|
763
|
+
type: "object",
|
|
764
|
+
properties: {
|
|
765
|
+
url: { type: "string", description: "The protected URL to test." },
|
|
766
|
+
redirectUrl: { type: "string", description: "The expected URL to be redirected to if unauthorized (e.g., '/login')." }
|
|
767
|
+
},
|
|
768
|
+
required: ["url", "redirectUrl"]
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
name: "audit_http_headers",
|
|
773
|
+
description: "Audits the current page's HTTP response headers for common security headers like CSP, HSTS, etc.",
|
|
774
|
+
parameters: { type: "object", properties: {} }
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
name: "scan_sensitive_data_exposure",
|
|
778
|
+
description: "Scans localStorage and sessionStorage for keys that might contain sensitive data (e.g., 'token', 'password').",
|
|
779
|
+
parameters: { type: "object", properties: {} }
|
|
780
|
+
}
|
|
781
|
+
];
|
|
640
782
|
var _sessionFieldCache = /* @__PURE__ */ new Set();
|
|
641
783
|
var AIAgentHelper = class {
|
|
642
784
|
page;
|
|
@@ -981,8 +1123,9 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
981
1123
|
}
|
|
982
1124
|
const memStep = m.steps_data[historyLength];
|
|
983
1125
|
if (!memStep) return false;
|
|
984
|
-
const
|
|
985
|
-
const
|
|
1126
|
+
const normalizeUrl = (u) => u.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/");
|
|
1127
|
+
const memUrl = normalizeUrl((memStep.url || "").split("?")[0]);
|
|
1128
|
+
const currUrl = normalizeUrl(currentUrl.split("?")[0]);
|
|
986
1129
|
if (memUrl !== currUrl) return false;
|
|
987
1130
|
const foundComponent = state.components.find((c) => {
|
|
988
1131
|
const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
|
|
@@ -1030,7 +1173,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
1030
1173
|
}
|
|
1031
1174
|
return null;
|
|
1032
1175
|
}
|
|
1033
|
-
async askIA(prompt, history = []) {
|
|
1176
|
+
async askIA(prompt, history = [], stepCount) {
|
|
1034
1177
|
if (!process.env.ARCALITY_API_URL && !process.env.ANTHROPIC_API_KEY) {
|
|
1035
1178
|
throw new Error("ANTHROPIC_API_KEY missing and ARCALITY_API_URL not configured");
|
|
1036
1179
|
}
|
|
@@ -1115,6 +1258,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
|
|
|
1115
1258
|
name: t.name,
|
|
1116
1259
|
description: t.description,
|
|
1117
1260
|
parameters: t.input_schema || t.parameters
|
|
1261
|
+
})),
|
|
1262
|
+
...qaSecurityTools.map((t) => ({
|
|
1263
|
+
name: t.name,
|
|
1264
|
+
description: t.description,
|
|
1265
|
+
parameters: t.input_schema || t.parameters
|
|
1118
1266
|
}))
|
|
1119
1267
|
];
|
|
1120
1268
|
const credentialsContext = process.env.LOGIN_USER && process.env.LOGIN_PASSWORD ? `
|
|
@@ -1304,12 +1452,15 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1304
1452
|
while (true) {
|
|
1305
1453
|
const isProxyMode = !!process.env.ARCALITY_API_URL;
|
|
1306
1454
|
const endpointUrl = isProxyMode ? `${process.env.ARCALITY_API_URL}/api/v1/ai/proxy` : "https://api.anthropic.com/v1/messages";
|
|
1307
|
-
|
|
1455
|
+
const displayTurn = stepCount !== void 0 ? stepCount : turn + 1;
|
|
1456
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F4E1} Llamando a: ${endpointUrl} (turno IA ${displayTurn}${turn > 0 ? `, reintento ${turn}` : ""})`);
|
|
1308
1457
|
const headers = {
|
|
1309
1458
|
"Content-Type": "application/json"
|
|
1310
1459
|
};
|
|
1311
1460
|
if (isProxyMode) {
|
|
1312
1461
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
1462
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
1463
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1313
1464
|
} else {
|
|
1314
1465
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
1315
1466
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -1610,6 +1761,73 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1610
1761
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error restoring checkpoint: ${e.message}` });
|
|
1611
1762
|
}
|
|
1612
1763
|
}
|
|
1764
|
+
} else if (toolName === "test_xss_injection") {
|
|
1765
|
+
const { idx, payload_type } = toolInput;
|
|
1766
|
+
const el = state.components.find((c) => c.idx === idx);
|
|
1767
|
+
if (!el) {
|
|
1768
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Element idx ${idx} not found.` });
|
|
1769
|
+
} else {
|
|
1770
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Probando inyecci\xF3n XSS en IDX ${idx}...`);
|
|
1771
|
+
let payload = "<script>alert(1)</script>";
|
|
1772
|
+
if (payload_type === "image") payload = "<img src=x onerror=alert(1)>";
|
|
1773
|
+
if (payload_type === "svg") payload = "<svg onload=alert(1)>";
|
|
1774
|
+
try {
|
|
1775
|
+
const locator = this.page.locator(`xpath=${el.selector}`).first();
|
|
1776
|
+
await locator.fill(payload);
|
|
1777
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Payload inyectado. Aseg\xFArate de probar si el payload fue ejecutado revisando el DOM, o realiza el sumbit y observa si el input regres\xF3 renderizado tal cual o sanitizado.` });
|
|
1778
|
+
if (this.testInfo) this.testInfo.attach("security_tool_call", { body: JSON.stringify({ action: "test_xss_injection", target: idx, payload }), contentType: "application/json" });
|
|
1779
|
+
} catch (e) {
|
|
1780
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error injecting payload: ${e.message}` });
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
} else if (toolName === "test_auth_bypass") {
|
|
1784
|
+
const { target_url } = toolInput;
|
|
1785
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Intentando Auth Bypass en ${target_url}...`);
|
|
1786
|
+
try {
|
|
1787
|
+
const baseUrl = new URL(this.page.url()).origin;
|
|
1788
|
+
const fullUrl = new URL(target_url, baseUrl).toString();
|
|
1789
|
+
await this.page.goto(fullUrl);
|
|
1790
|
+
await this.page.waitForLoadState("networkidle", { timeout: 3e3 }).catch(() => {
|
|
1791
|
+
});
|
|
1792
|
+
const resultingUrl = this.page.url();
|
|
1793
|
+
let resultData = `Navegado a ${fullUrl}.
|
|
1794
|
+
URL resultante: ${resultingUrl}.
|
|
1795
|
+
`;
|
|
1796
|
+
if (resultingUrl !== fullUrl) resultData += `Posiblemente redirigido (el bypass fall\xF3 o te enviaron a Login).`;
|
|
1797
|
+
else resultData += `URL se mantuvo igual. \xA1Esto podr\xEDa ser un IDOR o Auth Bypass si ves datos a los que no deber\xEDas acceder!`;
|
|
1798
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: resultData });
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error test_auth_bypass: ${e.message}` });
|
|
1801
|
+
}
|
|
1802
|
+
} else if (toolName === "scan_sensitive_data_exposure") {
|
|
1803
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Escaneando datos sensibles...`);
|
|
1804
|
+
try {
|
|
1805
|
+
const issues = await scan_sensitive_data_exposure(this.page);
|
|
1806
|
+
if (issues.length > 0) {
|
|
1807
|
+
const report = issues.map((i) => `- ${i.category}: ${i.description}
|
|
1808
|
+
Evidence: ${JSON.stringify(i.evidence)}`).join("\n");
|
|
1809
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades detectadas:
|
|
1810
|
+
${report}` });
|
|
1811
|
+
} else {
|
|
1812
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron datos sensibles en LocalStorage/SessionStorage en este chequeo." });
|
|
1813
|
+
}
|
|
1814
|
+
} catch (e) {
|
|
1815
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scan_sensitive_data_exposure: ${e.message}` });
|
|
1816
|
+
}
|
|
1817
|
+
} else if (toolName === "audit_http_headers") {
|
|
1818
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F [Security] Auditando headers HTTP...`);
|
|
1819
|
+
try {
|
|
1820
|
+
const issues = await audit_http_headers(this.page);
|
|
1821
|
+
if (issues.length > 0) {
|
|
1822
|
+
const report = issues.map((i) => `- ${i.category}: ${i.description}`).join("\n");
|
|
1823
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Vulnerabilidades de Headers detectadas:
|
|
1824
|
+
${report}` });
|
|
1825
|
+
} else {
|
|
1826
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: "No se detectaron problemas en los headers de seguridad b\xE1sicos." });
|
|
1827
|
+
}
|
|
1828
|
+
} catch (e) {
|
|
1829
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error audit_http_headers: ${e.message}` });
|
|
1830
|
+
}
|
|
1613
1831
|
} else if (toolName === "create_test_evidence") {
|
|
1614
1832
|
const { evidence_type, annotations, save_as, description, finish_run = false } = toolInput;
|
|
1615
1833
|
console.log(`\u{1F4F8} [SKILL] Creating test evidence: "${save_as}"...`);
|
|
@@ -1817,6 +2035,8 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1817
2035
|
} catch (e) {
|
|
1818
2036
|
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error scrolling: ${e.message}` });
|
|
1819
2037
|
}
|
|
2038
|
+
} else {
|
|
2039
|
+
toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: `Error: Tool '${toolName}' not recognized by this version.` });
|
|
1820
2040
|
}
|
|
1821
2041
|
}
|
|
1822
2042
|
if (toolResults.length > 0) {
|
|
@@ -1860,6 +2080,8 @@ ${history.slice(-25).join("\n") || "None"}`
|
|
|
1860
2080
|
};
|
|
1861
2081
|
if (isProxyMode) {
|
|
1862
2082
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
2083
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
2084
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1863
2085
|
} else {
|
|
1864
2086
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
1865
2087
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -1904,6 +2126,8 @@ ${history.join("\n")}` }
|
|
|
1904
2126
|
};
|
|
1905
2127
|
if (isProxyMode) {
|
|
1906
2128
|
headers["x-api-key"] = process.env.ARCALITY_API_KEY || "";
|
|
2129
|
+
const missionId = process.env.ARCALITY_MISSION_ID || "";
|
|
2130
|
+
if (missionId) headers["x-mission-id"] = missionId;
|
|
1907
2131
|
} else {
|
|
1908
2132
|
headers["x-api-key"] = process.env.ANTHROPIC_API_KEY;
|
|
1909
2133
|
headers["anthropic-version"] = "2023-06-01";
|
|
@@ -1954,6 +2178,101 @@ ${historyStr}` }
|
|
|
1954
2178
|
}
|
|
1955
2179
|
};
|
|
1956
2180
|
|
|
2181
|
+
// src/services/securityScanner.ts
|
|
2182
|
+
var SecurityScanner = class {
|
|
2183
|
+
page;
|
|
2184
|
+
report = [];
|
|
2185
|
+
constructor(page) {
|
|
2186
|
+
this.page = page;
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Runs a series of automated scans against the current state of the page.
|
|
2190
|
+
* This is intended to be called at the end of a test run to aggregate findings.
|
|
2191
|
+
* @returns A promise that resolves to an array of found vulnerabilities.
|
|
2192
|
+
*/
|
|
2193
|
+
async runAllScans() {
|
|
2194
|
+
console.log("[QA-SEC] Starting comprehensive security scan...");
|
|
2195
|
+
const headerVulnerabilities = await audit_http_headers(this.page);
|
|
2196
|
+
this.report.push(...headerVulnerabilities);
|
|
2197
|
+
const storageVulnerabilities = await scan_sensitive_data_exposure(this.page);
|
|
2198
|
+
this.report.push(...storageVulnerabilities);
|
|
2199
|
+
await this.testOpenRedirect();
|
|
2200
|
+
console.log(`[QA-SEC] Comprehensive scan finished. Found ${this.report.length} potential vulnerabilities.`);
|
|
2201
|
+
return this.getReport();
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* A basic test for open redirect vulnerabilities on the current URL.
|
|
2205
|
+
*/
|
|
2206
|
+
async testOpenRedirect() {
|
|
2207
|
+
const url = new URL(this.page.url());
|
|
2208
|
+
const redirectParams = ["redirect", "next", "url", "returnTo", "dest"];
|
|
2209
|
+
for (const param of redirectParams) {
|
|
2210
|
+
if (url.searchParams.has(param)) {
|
|
2211
|
+
const originalValue = url.searchParams.get(param);
|
|
2212
|
+
const payload = "https://www.evil-redirect.com";
|
|
2213
|
+
url.searchParams.set(param, payload);
|
|
2214
|
+
console.log(`[QA-SEC] Testing Open Redirect on param: ${param}`);
|
|
2215
|
+
try {
|
|
2216
|
+
await this.page.goto(url.toString(), { waitUntil: "networkidle" });
|
|
2217
|
+
if (this.page.url().startsWith(payload)) {
|
|
2218
|
+
const vulnerability = {
|
|
2219
|
+
type: "confirmed_vulnerability",
|
|
2220
|
+
category: "Open Redirect",
|
|
2221
|
+
severity: "Medium",
|
|
2222
|
+
confidence: "High",
|
|
2223
|
+
exploitability: "confirmed",
|
|
2224
|
+
status: "detected",
|
|
2225
|
+
description: `The application is vulnerable to Open Redirect. The '${param}' parameter was manipulated to redirect the user to an external, malicious site.`,
|
|
2226
|
+
impact: "Attackers can use this to craft phishing links that appear to come from the trusted domain but redirect to malicious sites.",
|
|
2227
|
+
evidence: {
|
|
2228
|
+
vulnerableUrl: url.toString(),
|
|
2229
|
+
parameter: param,
|
|
2230
|
+
payload
|
|
2231
|
+
},
|
|
2232
|
+
reproductionSteps: [
|
|
2233
|
+
`Navigate to: ${url.toString()}`,
|
|
2234
|
+
"Observe that the page redirects to the payload domain."
|
|
2235
|
+
],
|
|
2236
|
+
remediation: "Validate all redirection URLs against a whitelist of allowed domains. Do not blindly redirect to user-supplied URLs.",
|
|
2237
|
+
classification: {
|
|
2238
|
+
cwe: "CWE-601"
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
this.report.push(vulnerability);
|
|
2242
|
+
await this.page.goBack();
|
|
2243
|
+
}
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
console.log(`[QA-SEC] Error during open redirect test, this might be expected if the navigation was blocked. Continuing scan.`);
|
|
2246
|
+
await this.page.goBack();
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Adds a vulnerability to the report. Can be used by other parts of the system
|
|
2253
|
+
* to feed into this scanner's report.
|
|
2254
|
+
* @param vuln - The vulnerability to add.
|
|
2255
|
+
*/
|
|
2256
|
+
addVulnerability(vuln) {
|
|
2257
|
+
this.report.push(vuln);
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Returns the final list of vulnerabilities.
|
|
2261
|
+
*/
|
|
2262
|
+
getReport() {
|
|
2263
|
+
const uniqueReport = [];
|
|
2264
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2265
|
+
for (const vuln of this.report) {
|
|
2266
|
+
const key = `${vuln.category}|${vuln.description}|${vuln.evidence.selector || ""}`;
|
|
2267
|
+
if (!seen.has(key)) {
|
|
2268
|
+
uniqueReport.push(vuln);
|
|
2269
|
+
seen.add(key);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
return uniqueReport;
|
|
2273
|
+
}
|
|
2274
|
+
};
|
|
2275
|
+
|
|
1957
2276
|
// tests/_helpers/agentic-runner.spec.ts
|
|
1958
2277
|
var import_config = require("dotenv/config");
|
|
1959
2278
|
var fs2 = __toESM(require("fs"));
|
|
@@ -2204,7 +2523,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2204
2523
|
if (!response) {
|
|
2205
2524
|
stepCount++;
|
|
2206
2525
|
console.log(`>>ARCALITY_STATUS>> \u23F3 Turno IA ${stepCount} de ${maxSteps} (Gu\xEDa us\xF3 ${guideStepCount} turnos gratis)...`);
|
|
2207
|
-
response = await agent.askIA(prompt, history);
|
|
2526
|
+
response = await agent.askIA(prompt, history, stepCount);
|
|
2208
2527
|
}
|
|
2209
2528
|
console.log(`\u{1F9E0} Pensamiento: ${response.thought}`);
|
|
2210
2529
|
if (!response.finish && history.length >= 6) {
|
|
@@ -2412,6 +2731,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2412
2731
|
stepsData.push({
|
|
2413
2732
|
url: urlBeforeAction,
|
|
2414
2733
|
// USAR URL CAPTURADA ANTES
|
|
2734
|
+
url_normalized: urlBeforeAction.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2415
2735
|
componentName: compName,
|
|
2416
2736
|
componentType: step.type,
|
|
2417
2737
|
action_data: { action: step.action, value: step.value },
|
|
@@ -2439,6 +2759,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2439
2759
|
if (!response.actions || response.actions.length === 0) {
|
|
2440
2760
|
stepsData.push({
|
|
2441
2761
|
url: page.url(),
|
|
2762
|
+
url_normalized: page.url().replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2442
2763
|
componentName: "MISSION_END",
|
|
2443
2764
|
componentType: "FINISH_SIGNAL",
|
|
2444
2765
|
action_data: { action: "finish" },
|
|
@@ -2538,6 +2859,37 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2538
2859
|
});
|
|
2539
2860
|
}
|
|
2540
2861
|
}
|
|
2862
|
+
try {
|
|
2863
|
+
const scanner = new SecurityScanner(page);
|
|
2864
|
+
const securityReport = await scanner.runAllScans();
|
|
2865
|
+
if (securityReport && securityReport.length > 0) {
|
|
2866
|
+
await testInfo.attach("security_report", {
|
|
2867
|
+
body: JSON.stringify(securityReport, null, 2),
|
|
2868
|
+
contentType: "application/json"
|
|
2869
|
+
});
|
|
2870
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F Escaneo de seguridad completado. ${securityReport.length} problemas encontrados.`);
|
|
2871
|
+
try {
|
|
2872
|
+
for (const v of securityReport) {
|
|
2873
|
+
const sev = v.severity || "Info";
|
|
2874
|
+
const mappedSeverity = sev === "Critical" ? "critical" : sev === "High" ? "important" : "suggestion";
|
|
2875
|
+
await pushRule({
|
|
2876
|
+
rule_type: "SECURITY",
|
|
2877
|
+
title: `Security: ${v.category} (${v.severity})`,
|
|
2878
|
+
description: `${v.description}
|
|
2879
|
+
Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
|
|
2880
|
+
severity: mappedSeverity
|
|
2881
|
+
}).catch(() => {
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
} catch (e) {
|
|
2885
|
+
console.warn(">>ARCALITY_STATUS>> \u26A0\uFE0F Fall\xF3 persistir hallazgos de seguridad en Memoria Colectiva.");
|
|
2886
|
+
}
|
|
2887
|
+
} else {
|
|
2888
|
+
console.log(`>>ARCALITY_STATUS>> \u{1F6E1}\uFE0F Escaneo de seguridad completado. No se encontraron problemas.`);
|
|
2889
|
+
}
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
console.error("Error in Security Scan:", e.message);
|
|
2892
|
+
}
|
|
2541
2893
|
saveMissionResults();
|
|
2542
2894
|
if (!aiMarkedSuccess) {
|
|
2543
2895
|
throw new Error("Misi\xF3n no completada o finalizada con errores.");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { test, expect } from '@playwright/test';
|
|
2
2
|
import { AIAgentHelper, AgentAction } from './ai-agent-helper';
|
|
3
3
|
import { pushKnowledge, pushRule } from '../../src/services/collectiveMemoryService';
|
|
4
|
+
import { SecurityScanner } from '../../src/services/securityScanner';
|
|
4
5
|
import 'dotenv/config';
|
|
5
6
|
import * as fs from 'fs';
|
|
6
7
|
import * as path from 'path';
|
|
@@ -327,7 +328,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
327
328
|
// 2. CONSULTAR IA — ESTE turno SÍ cuenta contra maxSteps
|
|
328
329
|
stepCount++;
|
|
329
330
|
console.log(`>>ARCALITY_STATUS>> ⏳ Turno IA ${stepCount} de ${maxSteps} (Guía usó ${guideStepCount} turnos gratis)...`);
|
|
330
|
-
response = await agent.askIA(prompt, history);
|
|
331
|
+
response = await agent.askIA(prompt, history, stepCount);
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
console.log(`🧠 Pensamiento: ${response.thought}`);
|
|
@@ -580,6 +581,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
580
581
|
if (compName.length < 100) { // Skip guide storage for misidentified giant text blobs
|
|
581
582
|
stepsData.push({
|
|
582
583
|
url: urlBeforeAction, // USAR URL CAPTURADA ANTES
|
|
584
|
+
url_normalized: urlBeforeAction.replace(/\/[a-z]{2}-[A-Z]{2}\//g, '/{locale}/').replace(/\/[a-z]{2}\//g, '/{lang}/'),
|
|
583
585
|
componentName: compName,
|
|
584
586
|
componentType: (step as any).type,
|
|
585
587
|
action_data: { action: step.action, value: step.value },
|
|
@@ -616,6 +618,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
616
618
|
if (!response.actions || response.actions.length === 0) {
|
|
617
619
|
stepsData.push({
|
|
618
620
|
url: page.url(),
|
|
621
|
+
url_normalized: page.url().replace(/\/[a-z]{2}-[A-Z]{2}\//g, '/{locale}/').replace(/\/[a-z]{2}\//g, '/{lang}/'),
|
|
619
622
|
componentName: "MISSION_END",
|
|
620
623
|
componentType: "FINISH_SIGNAL",
|
|
621
624
|
action_data: { action: 'finish' },
|
|
@@ -739,6 +742,39 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
739
742
|
}
|
|
740
743
|
}
|
|
741
744
|
|
|
745
|
+
// --- SECURITY SCAN ---
|
|
746
|
+
try {
|
|
747
|
+
const scanner = new SecurityScanner(page);
|
|
748
|
+
const securityReport = await scanner.runAllScans();
|
|
749
|
+
|
|
750
|
+
if (securityReport && securityReport.length > 0) {
|
|
751
|
+
await testInfo.attach('security_report', {
|
|
752
|
+
body: JSON.stringify(securityReport, null, 2),
|
|
753
|
+
contentType: 'application/json'
|
|
754
|
+
});
|
|
755
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ Escaneo de seguridad completado. ${securityReport.length} problemas encontrados.`);
|
|
756
|
+
try {
|
|
757
|
+
// Persistir hallazgos en Memoria Colectiva como reglas de tipo SECURITY
|
|
758
|
+
for (const v of securityReport) {
|
|
759
|
+
const sev = (v.severity || 'Info') as string;
|
|
760
|
+
const mappedSeverity = sev === 'Critical' ? 'critical' : (sev === 'High' ? 'important' : 'suggestion');
|
|
761
|
+
await pushRule({
|
|
762
|
+
rule_type: 'SECURITY',
|
|
763
|
+
title: `Security: ${v.category} (${v.severity})`,
|
|
764
|
+
description: `${v.description}\nEvidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
|
|
765
|
+
severity: mappedSeverity as any
|
|
766
|
+
}).catch(() => { /* silencioso */ });
|
|
767
|
+
}
|
|
768
|
+
} catch (e) {
|
|
769
|
+
console.warn('>>ARCALITY_STATUS>> ⚠️ Falló persistir hallazgos de seguridad en Memoria Colectiva.');
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
console.log(`>>ARCALITY_STATUS>> 🛡️ Escaneo de seguridad completado. No se encontraron problemas.`);
|
|
773
|
+
}
|
|
774
|
+
} catch (e: any) {
|
|
775
|
+
console.error('Error in Security Scan:', e.message);
|
|
776
|
+
}
|
|
777
|
+
|
|
742
778
|
saveMissionResults();
|
|
743
779
|
|
|
744
780
|
if (!aiMarkedSuccess) {
|