@arcadialdev/arcality 2.4.23 → 2.4.25
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 -93
- package/package.json +68 -68
- package/playwright.config.js +33 -33
- package/scripts/gen-and-run.mjs +24 -13
- package/scripts/init.mjs +297 -296
- package/scripts/postinstall.mjs +77 -77
- package/scripts/setup.mjs +166 -166
- package/src/configLoader.mjs +179 -185
- 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 +632 -229
- package/tests/_helpers/ArcalityReporter.ts +262 -27
- package/tests/_helpers/agentic-runner.bundle.spec.js +366 -8
- package/tests/_helpers/agentic-runner.spec.ts +46 -4
- 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"));
|
|
@@ -2088,7 +2407,9 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2088
2407
|
const target = process.env.TARGET_PATH || "/";
|
|
2089
2408
|
if (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD) {
|
|
2090
2409
|
console.log(">>ARCALITY_STATUS>> \u{1F511} Realizando login autom\xE1tico...");
|
|
2091
|
-
|
|
2410
|
+
const loginUrl = base + "/login";
|
|
2411
|
+
if (!loginUrl.startsWith("http")) throw new Error(`URL Inv\xE1lida: "${loginUrl}". Aseg\xFArate de configurar la Base URL con http://`);
|
|
2412
|
+
await page.goto(loginUrl);
|
|
2092
2413
|
try {
|
|
2093
2414
|
const userInp = page.locator('input[type="email"], input[name="email"], input[name="username"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
|
|
2094
2415
|
await userInp.waitFor({ state: "visible", timeout: 1e4 });
|
|
@@ -2108,11 +2429,15 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2108
2429
|
console.warn(` \u26A0\uFE0F Login autom\xE1tico omitido o ya autenticado.`);
|
|
2109
2430
|
}
|
|
2110
2431
|
if (target !== "/" && !page.url().includes(target)) {
|
|
2111
|
-
|
|
2432
|
+
const tgtUrl = target.startsWith("http") ? target : base + target;
|
|
2433
|
+
if (!tgtUrl.startsWith("http")) throw new Error(`URL Inv\xE1lida: "${tgtUrl}"`);
|
|
2434
|
+
await page.goto(tgtUrl).catch(() => {
|
|
2112
2435
|
});
|
|
2113
2436
|
}
|
|
2114
2437
|
} else {
|
|
2115
|
-
|
|
2438
|
+
const tgtUrl = target.startsWith("http") ? target : base + target;
|
|
2439
|
+
if (!tgtUrl.startsWith("http")) throw new Error(`URL Inv\xE1lida: "${tgtUrl}". Por favor proporciona una URL v\xE1lida incluyendo http:// o https://`);
|
|
2440
|
+
await page.goto(tgtUrl);
|
|
2116
2441
|
}
|
|
2117
2442
|
await page.waitForLoadState("networkidle");
|
|
2118
2443
|
page.on("download", async (download) => {
|
|
@@ -2198,7 +2523,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2198
2523
|
if (!response) {
|
|
2199
2524
|
stepCount++;
|
|
2200
2525
|
console.log(`>>ARCALITY_STATUS>> \u23F3 Turno IA ${stepCount} de ${maxSteps} (Gu\xEDa us\xF3 ${guideStepCount} turnos gratis)...`);
|
|
2201
|
-
response = await agent.askIA(prompt, history);
|
|
2526
|
+
response = await agent.askIA(prompt, history, stepCount);
|
|
2202
2527
|
}
|
|
2203
2528
|
console.log(`\u{1F9E0} Pensamiento: ${response.thought}`);
|
|
2204
2529
|
if (!response.finish && history.length >= 6) {
|
|
@@ -2406,6 +2731,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2406
2731
|
stepsData.push({
|
|
2407
2732
|
url: urlBeforeAction,
|
|
2408
2733
|
// USAR URL CAPTURADA ANTES
|
|
2734
|
+
url_normalized: urlBeforeAction.replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2409
2735
|
componentName: compName,
|
|
2410
2736
|
componentType: step.type,
|
|
2411
2737
|
action_data: { action: step.action, value: step.value },
|
|
@@ -2433,6 +2759,7 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2433
2759
|
if (!response.actions || response.actions.length === 0) {
|
|
2434
2760
|
stepsData.push({
|
|
2435
2761
|
url: page.url(),
|
|
2762
|
+
url_normalized: page.url().replace(/\/[a-z]{2}-[A-Z]{2}\//g, "/{locale}/").replace(/\/[a-z]{2}\//g, "/{lang}/"),
|
|
2436
2763
|
componentName: "MISSION_END",
|
|
2437
2764
|
componentType: "FINISH_SIGNAL",
|
|
2438
2765
|
action_data: { action: "finish" },
|
|
@@ -2532,6 +2859,37 @@ function captureValidationRule(errorMessage, context) {
|
|
|
2532
2859
|
});
|
|
2533
2860
|
}
|
|
2534
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
|
+
}
|
|
2535
2893
|
saveMissionResults();
|
|
2536
2894
|
if (!aiMarkedSuccess) {
|
|
2537
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';
|
|
@@ -176,7 +177,9 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
176
177
|
|
|
177
178
|
if (process.env.LOGIN_USER && process.env.LOGIN_PASSWORD) {
|
|
178
179
|
console.log(">>ARCALITY_STATUS>> 🔑 Realizando login automático...");
|
|
179
|
-
|
|
180
|
+
const loginUrl = base + '/login';
|
|
181
|
+
if (!loginUrl.startsWith('http')) throw new Error(`URL Inválida: "${loginUrl}". Asegúrate de configurar la Base URL con http://`);
|
|
182
|
+
await page.goto(loginUrl);
|
|
180
183
|
try {
|
|
181
184
|
const userInp = page.locator('input[type="email"], input[name="email"], input[name="username"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
|
|
182
185
|
await userInp.waitFor({ state: 'visible', timeout: 10000 });
|
|
@@ -200,10 +203,14 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
200
203
|
}
|
|
201
204
|
|
|
202
205
|
if (target !== '/' && !page.url().includes(target)) {
|
|
203
|
-
|
|
206
|
+
const tgtUrl = target.startsWith('http') ? target : (base + target);
|
|
207
|
+
if (!tgtUrl.startsWith('http')) throw new Error(`URL Inválida: "${tgtUrl}"`);
|
|
208
|
+
await page.goto(tgtUrl).catch(() => { });
|
|
204
209
|
}
|
|
205
210
|
} else {
|
|
206
|
-
|
|
211
|
+
const tgtUrl = target.startsWith('http') ? target : (base + target);
|
|
212
|
+
if (!tgtUrl.startsWith('http')) throw new Error(`URL Inválida: "${tgtUrl}". Por favor proporciona una URL válida incluyendo http:// o https://`);
|
|
213
|
+
await page.goto(tgtUrl);
|
|
207
214
|
}
|
|
208
215
|
await page.waitForLoadState('networkidle');
|
|
209
216
|
|
|
@@ -321,7 +328,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
321
328
|
// 2. CONSULTAR IA — ESTE turno SÍ cuenta contra maxSteps
|
|
322
329
|
stepCount++;
|
|
323
330
|
console.log(`>>ARCALITY_STATUS>> ⏳ Turno IA ${stepCount} de ${maxSteps} (Guía usó ${guideStepCount} turnos gratis)...`);
|
|
324
|
-
response = await agent.askIA(prompt, history);
|
|
331
|
+
response = await agent.askIA(prompt, history, stepCount);
|
|
325
332
|
}
|
|
326
333
|
|
|
327
334
|
console.log(`🧠 Pensamiento: ${response.thought}`);
|
|
@@ -574,6 +581,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
574
581
|
if (compName.length < 100) { // Skip guide storage for misidentified giant text blobs
|
|
575
582
|
stepsData.push({
|
|
576
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}/'),
|
|
577
585
|
componentName: compName,
|
|
578
586
|
componentType: (step as any).type,
|
|
579
587
|
action_data: { action: step.action, value: step.value },
|
|
@@ -610,6 +618,7 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
610
618
|
if (!response.actions || response.actions.length === 0) {
|
|
611
619
|
stepsData.push({
|
|
612
620
|
url: page.url(),
|
|
621
|
+
url_normalized: page.url().replace(/\/[a-z]{2}-[A-Z]{2}\//g, '/{locale}/').replace(/\/[a-z]{2}\//g, '/{lang}/'),
|
|
613
622
|
componentName: "MISSION_END",
|
|
614
623
|
componentType: "FINISH_SIGNAL",
|
|
615
624
|
action_data: { action: 'finish' },
|
|
@@ -733,6 +742,39 @@ test('Arcality AI Runner', async ({ page }, testInfo) => {
|
|
|
733
742
|
}
|
|
734
743
|
}
|
|
735
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
|
+
|
|
736
778
|
saveMissionResults();
|
|
737
779
|
|
|
738
780
|
if (!aiMarkedSuccess) {
|