@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.
@@ -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"));
@@ -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) {