@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.
@@ -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 memUrl = (memStep.url || "").split("?")[0];
985
- const currUrl = currentUrl.split("?")[0];
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
- console.log(`>>ARCALITY_STATUS>> \u{1F4E1} Llamando a: ${endpointUrl} (turno ${turn + 1})`);
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
- await page.goto(base + "/login");
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
- await page.goto(base + target).catch(() => {
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
- await page.goto(base + target);
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
- await page.goto(base + '/login');
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
- await page.goto(base + target).catch(() => { });
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
- await page.goto(base + target);
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) {