No Description

app.js 39KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252
  1. function defaultLogLossStreams() {
  2. return [
  3. { name: "log_monitor", query: "full_log:log_monitor OR rule.id:100411", min_count: 1 },
  4. {
  5. name: "fortigate",
  6. query: "full_log:fortigate OR full_log:FGT80F OR full_log:FGT60F OR full_log:FGT40F OR full_log:FGT501E",
  7. min_count: 1,
  8. },
  9. { name: "windows_agent", query: "full_log:windows_agent OR full_log:windows", min_count: 1 },
  10. ];
  11. }
  12. function parseJsonOrThrow(text, label) {
  13. const raw = (text || "").trim();
  14. if (!raw) {
  15. return {};
  16. }
  17. try {
  18. return JSON.parse(raw);
  19. } catch (_err) {
  20. throw new Error(`Invalid JSON in ${label}`);
  21. }
  22. }
  23. const SIM_SCRIPT_DESCRIPTIONS = {
  24. fortigate: "FortiGate firewall telemetry simulator (40F/60F/80F/501E) for network and security events.",
  25. endpoint: "Windows/macOS/Linux endpoint-agent simulator for process, auth, and endpoint behavior logs.",
  26. cisco: "Cisco ASA/IOS network log simulator for parser/rule coverage and telemetry validation.",
  27. proposal_required: "Appendix A required-use-case simulator (A1-A4) for proposal/UAT coverage.",
  28. proposal_appendix_b: "Appendix B optional-use-case simulator (B1-B3), including log-loss test flow.",
  29. proposal_appendix_c: "Appendix C future-enhancement simulator (C1-C3 currently implemented).",
  30. wazuh_test: "Generic Wazuh baseline test events for connectivity and ingest sanity checks.",
  31. };
  32. const SIM_SCRIPT_TARGET_OPTIONS = {
  33. fortigate: ["all", "501E", "80F", "60F", "40F"],
  34. endpoint: ["all", "windows", "mac", "linux"],
  35. cisco: ["all", "asa_acl_deny", "asa_vpn_auth_fail", "ios_login_fail", "ios_config_change"],
  36. proposal_required: ["all", "a1", "a2", "a3", "a4"],
  37. proposal_appendix_b: ["all", "b1", "b2", "b3"],
  38. proposal_appendix_c: ["all", "c1", "c2", "c3"],
  39. wazuh_test: ["all", "ioc_dns", "ioc_ips", "vpn_outside_th", "windows_auth_fail"],
  40. };
  41. window.socUi = function socUi() {
  42. return {
  43. tabs: [
  44. { key: "overview", label: "Overview" },
  45. { key: "systems", label: "Systems" },
  46. { key: "database", label: "Database" },
  47. { key: "monitoring", label: "Monitoring" },
  48. { key: "ioc", label: "IOC" },
  49. { key: "geoip", label: "GeoIP" },
  50. { key: "iris", label: "IRIS" },
  51. { key: "shuffle", label: "Shuffle" },
  52. { key: "wazuh", label: "Wazuh" },
  53. { key: "mvp", label: "MVP" },
  54. { key: "explorer", label: "API Explorer" },
  55. ],
  56. activeTab: "overview",
  57. apiBase: window.location.origin,
  58. internalApiKey: "dev-internal-key",
  59. errorMessage: "",
  60. overview: { health: null, autoSync: null },
  61. systemsMonitor: {
  62. data: null,
  63. loading: false,
  64. autoRefresh: true,
  65. paused: false,
  66. intervalSeconds: 20,
  67. minutes: 60,
  68. limit: 20,
  69. lastRefreshAt: null,
  70. timerId: null,
  71. },
  72. dbTables: {
  73. data: null,
  74. loading: false,
  75. lastRefreshAt: null,
  76. },
  77. dbBrowser: {
  78. selectedTable: "",
  79. rows: null,
  80. loading: false,
  81. limit: 50,
  82. offset: 0,
  83. },
  84. simLogs: {
  85. runs: null,
  86. startResult: null,
  87. selectedRunId: "",
  88. output: null,
  89. outputLimit: 200,
  90. autoRefresh: true,
  91. intervalSeconds: 3,
  92. timerId: null,
  93. form: {
  94. script: "fortigate",
  95. targets: ["all"],
  96. scenario: "all",
  97. count: 1,
  98. delay_seconds: 0.3,
  99. forever: false,
  100. },
  101. },
  102. simWazuh: {
  103. latest: null,
  104. limit: 100,
  105. autoRefresh: true,
  106. showQuery: false,
  107. },
  108. systemsCardMeta: [
  109. { key: "wazuh", label: "Wazuh" },
  110. { key: "shuffle", label: "Shuffle" },
  111. { key: "iris", label: "IRIS" },
  112. { key: "pagerduty", label: "PagerDuty" },
  113. ],
  114. logLoss: { result: null },
  115. cDetections: { state: null, evaluate: null, history: null },
  116. ioc: { enrich: null, evaluate: null, history: null, upload: null, analysis: null, fileEval: null },
  117. geoip: { ip: "8.8.8.8", result: null },
  118. iris: { create: null, list: null },
  119. shuffle: { status: null, catalog: null, execute: null },
  120. wazuh: { status: null, list: null, sync: null },
  121. mvp: {
  122. status: null,
  123. ingest: null,
  124. evaluate: null,
  125. policyText: JSON.stringify({ risk_thresholds: { high: 80, medium: 50 } }, null, 2),
  126. ingestText: JSON.stringify(
  127. {
  128. source: "wazuh",
  129. event_type: "vpn_geo_anomaly",
  130. event_id: "evt-demo-1",
  131. timestamp: new Date().toISOString(),
  132. severity: "high",
  133. title: "VPN login anomaly",
  134. description: "User login from unusual country",
  135. asset: { user: "alice", hostname: "win-dc01" },
  136. network: { src_ip: "8.8.8.8", country: "US" },
  137. tags: ["vpn", "geo-anomaly"],
  138. risk_context: { admin_account: false },
  139. raw: { full_log: "soc_mvp_test=true" },
  140. payload: {},
  141. },
  142. null,
  143. 2,
  144. ),
  145. iocEvalText: JSON.stringify({ ioc_type: "ip", ioc_value: "8.8.8.8", source_event: { event_id: "evt-demo-1" } }, null, 2),
  146. vpnEvalText: JSON.stringify(
  147. {
  148. user: "alice",
  149. src_ip: "8.8.8.8",
  150. country_code: "US",
  151. success: true,
  152. event_time: new Date().toISOString(),
  153. is_admin: false,
  154. off_hours: true,
  155. first_seen_country: true,
  156. event_id: "vpn-demo-1",
  157. },
  158. null,
  159. 2,
  160. ),
  161. },
  162. logLossForm: { minutes: 5, createTicket: false, streams: defaultLogLossStreams() },
  163. cEvalForm: { minutes: 30, query: "soc_mvp_test=true", selectorsText: "c1,c2,c3", dry_run: true, limit: 200 },
  164. iocForm: {
  165. ioc_type: "ip",
  166. ioc_value: "8.8.8.8",
  167. providersText: "virustotal,abuseipdb",
  168. malicious_threshold: 1,
  169. suspicious_threshold: 3,
  170. },
  171. iocFileForm: { file: null, analysis_id: "", poll_timeout_seconds: 30, poll_interval_seconds: 2 },
  172. irisForm: {
  173. title: "SOC Demo Ticket",
  174. description: "Created from SOC Integrator UI",
  175. case_customer: 1,
  176. case_soc_id: "soc-prod",
  177. },
  178. irisList: { limit: 50, offset: 0 },
  179. shuffleExec: { workflow_id: "", payloadText: JSON.stringify({ hello: "world" }, null, 2) },
  180. wazuhList: { limit: 50, offset: 0, q: "" },
  181. wazuhSync: { minutes: 120, limit: 50, q: "soc_mvp_test=true OR event_type:*" },
  182. explorer: {
  183. spec: null,
  184. endpoints: [],
  185. selectedKey: "",
  186. selected: null,
  187. pathParamsText: "{}",
  188. queryText: "{}",
  189. bodyText: "{}",
  190. result: null,
  191. },
  192. tabClass(key) {
  193. return this.activeTab === key
  194. ? "bg-sky-600 text-white border-sky-600"
  195. : "bg-white text-slate-700 hover:bg-slate-50";
  196. },
  197. init() {
  198. this.loadHealth();
  199. this.loadAutoSync();
  200. this.loadCState();
  201. this.loadSystemsMonitor();
  202. this.loadDbTables();
  203. this.loadSimRuns();
  204. this.startSimLogsAutoRefresh();
  205. this.startSystemsMonitorAutoRefresh();
  206. this.loadOpenApiSpec();
  207. },
  208. simScriptUsesScenario() {
  209. return this.simLogs.form.script === "endpoint";
  210. },
  211. simScriptDescription() {
  212. const key = String(this.simLogs.form.script || "").trim();
  213. return SIM_SCRIPT_DESCRIPTIONS[key] || "No description available for this script.";
  214. },
  215. simTargetOptions() {
  216. const key = String(this.simLogs.form.script || "").trim();
  217. return SIM_SCRIPT_TARGET_OPTIONS[key] || ["all"];
  218. },
  219. simTargetSelectionChanged() {
  220. const options = this.simTargetOptions();
  221. const selected = Array.isArray(this.simLogs.form.targets) ? [...this.simLogs.form.targets] : [];
  222. let valid = selected.filter((item) => options.includes(item));
  223. if (valid.includes("all") && valid.length > 1) {
  224. valid = ["all"];
  225. }
  226. if (!valid.length) {
  227. valid = ["all"];
  228. }
  229. this.simLogs.form.targets = valid;
  230. },
  231. onSimScriptChange() {
  232. this.simTargetSelectionChanged();
  233. },
  234. selectedTargetsForRun() {
  235. const options = this.simTargetOptions();
  236. let selected = Array.isArray(this.simLogs.form.targets) ? [...this.simLogs.form.targets] : [];
  237. selected = selected.filter((item) => options.includes(item));
  238. if (!selected.length || selected.includes("all")) {
  239. return ["all"];
  240. }
  241. return selected;
  242. },
  243. systemsStatusClass(status) {
  244. if (status === "ok") {
  245. return "status-ok";
  246. }
  247. if (status === "degraded") {
  248. return "status-warn";
  249. }
  250. return "status-down";
  251. },
  252. systemsCards() {
  253. const root = this.unwrapApiData(this.systemsMonitor.data) || {};
  254. return root.cards || {};
  255. },
  256. systemsCard(key) {
  257. const cards = this.systemsCards();
  258. return cards[key] || {};
  259. },
  260. systemsRecentRows(key) {
  261. const recent = this.systemsCard(key).recent;
  262. if (!Array.isArray(recent)) {
  263. return [];
  264. }
  265. return recent.map((row, index) => this.normalizeTableRow(row, index));
  266. },
  267. systemsRecentColumns(key) {
  268. return this.tableColumns(this.systemsRecentRows(key));
  269. },
  270. systemsPipelineRows() {
  271. const root = this.unwrapApiData(this.systemsMonitor.data) || {};
  272. const pipeline = root.pipeline || {};
  273. return Object.entries(pipeline).map(([key, value]) => ({ key, value: this.cellText(value) }));
  274. },
  275. systemsSetAutoRefresh(enabled) {
  276. this.systemsMonitor.autoRefresh = Boolean(enabled);
  277. this.startSystemsMonitorAutoRefresh();
  278. },
  279. systemsTogglePaused() {
  280. this.systemsMonitor.paused = !this.systemsMonitor.paused;
  281. },
  282. systemsSetInterval(seconds) {
  283. const parsed = Number(seconds || 20);
  284. this.systemsMonitor.intervalSeconds = parsed > 0 ? parsed : 20;
  285. this.startSystemsMonitorAutoRefresh();
  286. },
  287. stopSystemsMonitorAutoRefresh() {
  288. if (this.systemsMonitor.timerId) {
  289. clearInterval(this.systemsMonitor.timerId);
  290. this.systemsMonitor.timerId = null;
  291. }
  292. },
  293. startSystemsMonitorAutoRefresh() {
  294. this.stopSystemsMonitorAutoRefresh();
  295. if (!this.systemsMonitor.autoRefresh) {
  296. return;
  297. }
  298. this.systemsMonitor.timerId = setInterval(() => {
  299. if (!this.systemsMonitor.paused) {
  300. this.loadSystemsMonitor();
  301. }
  302. }, Math.max(5, Number(this.systemsMonitor.intervalSeconds || 20)) * 1000);
  303. },
  304. async loadSystemsMonitor() {
  305. try {
  306. if (!this.internalApiKey) {
  307. return;
  308. }
  309. this.systemsMonitor.loading = true;
  310. const params = new URLSearchParams({
  311. minutes: String(Math.max(1, Number(this.systemsMonitor.minutes || 60))),
  312. limit: String(Math.max(1, Number(this.systemsMonitor.limit || 20))),
  313. });
  314. this.systemsMonitor.data = await this.apiCall(`/monitor/systems?${params.toString()}`, {
  315. internal: true,
  316. });
  317. this.systemsMonitor.lastRefreshAt = new Date().toISOString();
  318. } catch (err) {
  319. this.setErr("Systems monitor failed", err);
  320. } finally {
  321. this.systemsMonitor.loading = false;
  322. }
  323. },
  324. async loadDbTables() {
  325. try {
  326. if (!this.internalApiKey) {
  327. return;
  328. }
  329. this.dbTables.loading = true;
  330. this.dbTables.data = await this.apiCall("/monitor/db/tables", {
  331. internal: true,
  332. });
  333. this.dbTables.lastRefreshAt = new Date().toISOString();
  334. if (!this.dbBrowser.selectedTable) {
  335. const tables = this.dbTableRows();
  336. if (tables.length > 0) {
  337. this.dbBrowser.selectedTable = String(tables[0].table || "");
  338. await this.loadDbRows();
  339. }
  340. }
  341. } catch (err) {
  342. this.setErr("Database tables failed", err);
  343. } finally {
  344. this.dbTables.loading = false;
  345. }
  346. },
  347. dbTableRows() {
  348. const root = this.unwrapApiData(this.dbTables.data) || {};
  349. const tables = Array.isArray(root.tables) ? root.tables : [];
  350. return tables.map((row, index) => this.normalizeTableRow(row, index));
  351. },
  352. async loadDbRows() {
  353. try {
  354. const table = String(this.dbBrowser.selectedTable || "").trim();
  355. if (!table) {
  356. this.dbBrowser.rows = null;
  357. return;
  358. }
  359. this.dbBrowser.loading = true;
  360. const limit = Math.max(1, Math.min(500, Number(this.dbBrowser.limit || 50)));
  361. const offset = Math.max(0, Number(this.dbBrowser.offset || 0));
  362. this.dbBrowser.rows = await this.apiCall(
  363. `/monitor/db/tables/${encodeURIComponent(table)}/rows?limit=${limit}&offset=${offset}`,
  364. { internal: true },
  365. );
  366. } catch (err) {
  367. this.setErr("Database rows failed", err);
  368. } finally {
  369. this.dbBrowser.loading = false;
  370. }
  371. },
  372. dbSelectedRows() {
  373. const root = this.unwrapApiData(this.dbBrowser.rows) || {};
  374. const rows = Array.isArray(root.rows) ? root.rows : [];
  375. return rows.map((row, index) => this.normalizeTableRow(row, index));
  376. },
  377. dbSelectedColumns() {
  378. return this.tableColumns(this.dbSelectedRows());
  379. },
  380. async loadSimRuns() {
  381. try {
  382. if (!this.internalApiKey) {
  383. return;
  384. }
  385. this.simLogs.runs = await this.apiCall("/sim/logs/runs", { internal: true });
  386. const rows = this.extractRows(this.simLogs.runs);
  387. if (!this.simLogs.selectedRunId && rows.length) {
  388. this.simLogs.selectedRunId = rows[0].run_id || "";
  389. if (this.simLogs.selectedRunId) {
  390. await this.loadSimWazuhLatest(this.simLogs.selectedRunId);
  391. }
  392. }
  393. } catch (err) {
  394. this.setErr("Load sim runs failed", err);
  395. }
  396. },
  397. async startSimRun() {
  398. try {
  399. const targets = this.selectedTargetsForRun();
  400. const results = [];
  401. for (const target of targets) {
  402. const payload = {
  403. script: this.simLogs.form.script,
  404. target,
  405. scenario: this.simLogs.form.scenario || "all",
  406. count: Number(this.simLogs.form.count || 1),
  407. delay_seconds: Number(this.simLogs.form.delay_seconds || 0.3),
  408. forever: Boolean(this.simLogs.form.forever),
  409. };
  410. const result = await this.apiCall("/sim/logs/start", {
  411. method: "POST",
  412. internal: true,
  413. json: payload,
  414. });
  415. results.push({ target, result });
  416. }
  417. this.simLogs.startResult = {
  418. data: {
  419. started_targets: targets,
  420. runs: results.map((item) => ({
  421. target: item.target,
  422. run: this.unwrapApiData(item.result)?.run || null,
  423. })),
  424. },
  425. };
  426. await this.loadSimRuns();
  427. const started = results
  428. .map((item) => this.unwrapApiData(item.result)?.run)
  429. .find((run) => run && run.run_id);
  430. if (started && started.run_id) {
  431. this.simLogs.selectedRunId = started.run_id;
  432. await this.loadSimOutput(started.run_id);
  433. await this.loadSimWazuhLatest(started.run_id);
  434. }
  435. } catch (err) {
  436. this.setErr("Start sim run failed", err);
  437. }
  438. },
  439. async stopSimRun(runId) {
  440. try {
  441. await this.apiCall(`/sim/logs/stop/${encodeURIComponent(runId)}`, {
  442. method: "POST",
  443. internal: true,
  444. });
  445. await this.loadSimRuns();
  446. if (this.simLogs.selectedRunId === runId) {
  447. await this.loadSimOutput(runId);
  448. await this.loadSimWazuhLatest(runId);
  449. }
  450. } catch (err) {
  451. this.setErr("Stop sim run failed", err);
  452. }
  453. },
  454. async stopRunningSimRuns() {
  455. try {
  456. await this.apiCall("/sim/logs/stop-running", {
  457. method: "POST",
  458. internal: true,
  459. });
  460. await this.loadSimRuns();
  461. if (this.simLogs.selectedRunId) {
  462. await this.loadSimOutput(this.simLogs.selectedRunId);
  463. await this.loadSimWazuhLatest(this.simLogs.selectedRunId);
  464. }
  465. } catch (err) {
  466. this.setErr("Stop running sim runs failed", err);
  467. }
  468. },
  469. simRunRows() {
  470. return this.extractRows(this.simLogs.runs);
  471. },
  472. selectSimRun(runId) {
  473. this.simLogs.selectedRunId = runId || "";
  474. this.loadSimOutput(this.simLogs.selectedRunId);
  475. this.loadSimWazuhLatest(this.simLogs.selectedRunId);
  476. },
  477. simSelectedRun() {
  478. const rows = this.simRunRows();
  479. return rows.find((row) => row.run_id === this.simLogs.selectedRunId) || null;
  480. },
  481. stopSimLogsAutoRefresh() {
  482. if (this.simLogs.timerId) {
  483. clearInterval(this.simLogs.timerId);
  484. this.simLogs.timerId = null;
  485. }
  486. },
  487. startSimLogsAutoRefresh() {
  488. this.stopSimLogsAutoRefresh();
  489. if (!this.simLogs.autoRefresh) {
  490. return;
  491. }
  492. this.simLogs.timerId = setInterval(async () => {
  493. if (this.activeTab !== "systems") {
  494. return;
  495. }
  496. try {
  497. await this.loadSimRuns();
  498. if (this.simLogs.selectedRunId) {
  499. await this.loadSimOutput(this.simLogs.selectedRunId);
  500. await this.loadSimWazuhLatest(this.simLogs.selectedRunId);
  501. }
  502. } catch (_err) {
  503. // per-request error is already handled by each loader
  504. }
  505. }, 5000);
  506. },
  507. async loadSimOutput(runId = "") {
  508. const selectedRunId = runId || this.simLogs.selectedRunId;
  509. if (!selectedRunId) {
  510. this.simLogs.output = null;
  511. return;
  512. }
  513. try {
  514. const limit = Math.max(10, Number(this.simLogs.outputLimit || 200));
  515. this.simLogs.output = await this.apiCall(
  516. `/sim/logs/output/${encodeURIComponent(selectedRunId)}?limit=${limit}`,
  517. { internal: true },
  518. );
  519. } catch (err) {
  520. this.setErr("Load sim output failed", err);
  521. }
  522. },
  523. async loadSimWazuhLatest(runId = "") {
  524. const selectedRunId = runId || this.simLogs.selectedRunId;
  525. if (!selectedRunId) {
  526. this.simWazuh.latest = null;
  527. return;
  528. }
  529. try {
  530. const limit = 100;
  531. this.simWazuh.latest = await this.apiCall(
  532. `/sim/logs/wazuh-latest/${encodeURIComponent(selectedRunId)}?limit=${limit}&minutes=1440&include_raw=true`,
  533. { internal: true },
  534. );
  535. } catch (err) {
  536. this.setErr("Load sim Wazuh latest failed", err);
  537. }
  538. },
  539. simWazuhEventsRows() {
  540. const root = this.unwrapApiData(this.simWazuh.latest) || {};
  541. const events = Array.isArray(root.events) ? root.events : [];
  542. return events.map((row, index) => this.normalizeTableRow(row, index));
  543. },
  544. simWazuhRulesRows() {
  545. const root = this.unwrapApiData(this.simWazuh.latest) || {};
  546. const rules = Array.isArray(root.rules) ? root.rules : [];
  547. return rules.map((row, index) => this.normalizeTableRow(row, index));
  548. },
  549. simWazuhEventTableRows() {
  550. return this.simWazuhEventsRows().map((row) => ({
  551. time: row["@timestamp"] || row.timestamp || "",
  552. rule_id: row.rule_id || row.rule?.id || "",
  553. rule_description: row.rule_description || row.rule?.description || "",
  554. full_log: row.full_log || "",
  555. }));
  556. },
  557. parseFullLog(fullLog) {
  558. const text = String(fullLog || "").trim();
  559. if (!text) {
  560. return {};
  561. }
  562. const parsed = {};
  563. const regex = /([A-Za-z0-9_.-]+)=("([^"]*)"|[^\s]+)/g;
  564. let match = null;
  565. while ((match = regex.exec(text)) !== null) {
  566. const key = match[1];
  567. let value = match[3] !== undefined ? match[3] : match[2];
  568. if (value === "true") {
  569. value = true;
  570. } else if (value === "false") {
  571. value = false;
  572. } else if (/^-?\d+$/.test(value)) {
  573. value = Number(value);
  574. } else if (/^-?\d+\.\d+$/.test(value)) {
  575. value = Number(value);
  576. }
  577. parsed[key] = value;
  578. }
  579. if (Object.keys(parsed).length === 0) {
  580. return { message: text };
  581. }
  582. return parsed;
  583. },
  584. fullLogAsJsonText(fullLog) {
  585. return JSON.stringify(this.parseFullLog(fullLog), null, 2);
  586. },
  587. pretty(value) {
  588. if (value === null || value === undefined) {
  589. return "No data yet";
  590. }
  591. try {
  592. return JSON.stringify(value, null, 2);
  593. } catch (_err) {
  594. return String(value);
  595. }
  596. },
  597. unwrapApiData(payload) {
  598. if (payload && typeof payload === "object" && !Array.isArray(payload) && Object.prototype.hasOwnProperty.call(payload, "data")) {
  599. return payload.data;
  600. }
  601. return payload;
  602. },
  603. findFirstArray(node, depth = 0) {
  604. if (depth > 5 || node === null || node === undefined) {
  605. return null;
  606. }
  607. if (Array.isArray(node)) {
  608. return node;
  609. }
  610. if (typeof node !== "object") {
  611. return null;
  612. }
  613. const preferredKeys = ["items", "matches", "results", "alerts", "agents", "workflows", "apps", "rows", "data"];
  614. for (const key of preferredKeys) {
  615. if (Array.isArray(node[key])) {
  616. return node[key];
  617. }
  618. }
  619. for (const value of Object.values(node)) {
  620. const found = this.findFirstArray(value, depth + 1);
  621. if (found) {
  622. return found;
  623. }
  624. }
  625. return null;
  626. },
  627. normalizeTableRow(row, index) {
  628. if (row && typeof row === "object" && !Array.isArray(row)) {
  629. return row;
  630. }
  631. return { row: index + 1, value: row };
  632. },
  633. extractRows(payload) {
  634. const root = this.unwrapApiData(payload);
  635. const rows = this.findFirstArray(root) || [];
  636. return rows.map((row, index) => this.normalizeTableRow(row, index));
  637. },
  638. tableColumns(rows) {
  639. const colSet = new Set();
  640. const sample = rows.slice(0, 30);
  641. for (const row of sample) {
  642. if (!row || typeof row !== "object" || Array.isArray(row)) {
  643. continue;
  644. }
  645. for (const key of Object.keys(row)) {
  646. if (colSet.size >= 10) {
  647. break;
  648. }
  649. colSet.add(key);
  650. }
  651. if (colSet.size >= 10) {
  652. break;
  653. }
  654. }
  655. return Array.from(colSet);
  656. },
  657. cellText(value) {
  658. if (value === null || value === undefined) {
  659. return "";
  660. }
  661. if (typeof value === "string") {
  662. return value;
  663. }
  664. if (typeof value === "number" || typeof value === "boolean") {
  665. return String(value);
  666. }
  667. return JSON.stringify(value);
  668. },
  669. keyValueRows(payload) {
  670. const root = this.unwrapApiData(payload);
  671. if (!root || typeof root !== "object" || Array.isArray(root)) {
  672. return [];
  673. }
  674. return Object.entries(root).map(([key, value]) => ({
  675. key,
  676. value: this.cellText(value),
  677. }));
  678. },
  679. providersList() {
  680. return (this.iocForm.providersText || "")
  681. .split(",")
  682. .map((s) => s.trim().toLowerCase())
  683. .filter((s) => s.length > 0);
  684. },
  685. internalHeaders() {
  686. return this.internalApiKey ? { "x-internal-api-key": this.internalApiKey } : {};
  687. },
  688. async apiCall(path, options = {}) {
  689. this.errorMessage = "";
  690. const headers = {
  691. Accept: "application/json",
  692. ...(options.json ? { "Content-Type": "application/json" } : {}),
  693. ...(options.internal ? this.internalHeaders() : {}),
  694. ...(options.headers || {}),
  695. };
  696. const response = await fetch(`${this.apiBase}${path}`, {
  697. method: options.method || "GET",
  698. headers,
  699. body: options.json ? JSON.stringify(options.json) : options.body,
  700. });
  701. let data = null;
  702. try {
  703. data = await response.json();
  704. } catch (_err) {
  705. data = { detail: "Non-JSON response", status: response.status };
  706. }
  707. if (!response.ok) {
  708. const detail = data && data.detail ? data.detail : `HTTP ${response.status}`;
  709. throw new Error(detail);
  710. }
  711. return data;
  712. },
  713. setErr(prefix, err) {
  714. this.errorMessage = `${prefix}: ${err.message}`;
  715. },
  716. async loadHealth() {
  717. try {
  718. this.overview.health = await this.apiCall("/health");
  719. } catch (err) {
  720. this.setErr("Health failed", err);
  721. }
  722. },
  723. async loadAutoSync() {
  724. try {
  725. this.overview.autoSync = await this.apiCall("/wazuh/auto-sync/status");
  726. } catch (err) {
  727. this.setErr("Auto-sync failed", err);
  728. }
  729. },
  730. applyLogLossPreset(preset) {
  731. if (preset === "b2") {
  732. this.logLossForm.minutes = 10;
  733. this.logLossForm.streams = [{ name: "b2_log_monitor", query: "full_log:log_monitor OR rule.id:100411", min_count: 1 }];
  734. } else {
  735. this.logLossForm.minutes = 5;
  736. this.logLossForm.streams = defaultLogLossStreams();
  737. }
  738. },
  739. addLogLossStream() {
  740. this.logLossForm.streams.push({ name: "", query: "", min_count: 1 });
  741. },
  742. removeLogLossStream(index) {
  743. this.logLossForm.streams.splice(index, 1);
  744. },
  745. async runLogLossCheck() {
  746. try {
  747. this.logLoss.result = await this.apiCall(
  748. `/monitor/log-loss/check?create_ticket=${this.logLossForm.createTicket ? "true" : "false"}`,
  749. {
  750. method: "POST",
  751. internal: true,
  752. json: {
  753. minutes: Number(this.logLossForm.minutes || 5),
  754. streams: this.logLossForm.streams.map((s) => ({
  755. name: s.name,
  756. query: s.query,
  757. min_count: Number(s.min_count || 0),
  758. })),
  759. },
  760. },
  761. );
  762. } catch (err) {
  763. this.setErr("Log-loss check failed", err);
  764. }
  765. },
  766. async loadCState() {
  767. try {
  768. this.cDetections.state = await this.apiCall("/monitor/c-detections/state", { internal: true });
  769. } catch (err) {
  770. this.setErr("C state failed", err);
  771. }
  772. },
  773. async runCEvaluate() {
  774. try {
  775. const selectors = this.cEvalForm.selectorsText
  776. .split(",")
  777. .map((x) => x.trim())
  778. .filter((x) => x.length > 0);
  779. this.cDetections.evaluate = await this.apiCall("/monitor/c-detections/evaluate", {
  780. method: "POST",
  781. internal: true,
  782. json: {
  783. minutes: Number(this.cEvalForm.minutes || 30),
  784. query: this.cEvalForm.query,
  785. selectors,
  786. dry_run: Boolean(this.cEvalForm.dry_run),
  787. limit: Number(this.cEvalForm.limit || 200),
  788. },
  789. });
  790. } catch (err) {
  791. this.setErr("C evaluate failed", err);
  792. }
  793. },
  794. async loadCHistory() {
  795. try {
  796. this.cDetections.history = await this.apiCall("/monitor/c-detections/history?limit=50&offset=0", { internal: true });
  797. } catch (err) {
  798. this.setErr("C history failed", err);
  799. }
  800. },
  801. async runIocEnrich() {
  802. try {
  803. this.ioc.enrich = await this.apiCall("/ioc/enrich", {
  804. method: "POST",
  805. json: {
  806. ioc_type: this.iocForm.ioc_type,
  807. ioc_value: this.iocForm.ioc_value,
  808. providers: this.providersList(),
  809. },
  810. });
  811. } catch (err) {
  812. this.setErr("IOC enrich failed", err);
  813. }
  814. },
  815. async runIocEvaluate() {
  816. try {
  817. this.ioc.evaluate = await this.apiCall("/ioc/evaluate", {
  818. method: "POST",
  819. json: {
  820. ioc_type: this.iocForm.ioc_type,
  821. ioc_value: this.iocForm.ioc_value,
  822. providers: this.providersList(),
  823. malicious_threshold: Number(this.iocForm.malicious_threshold || 1),
  824. suspicious_threshold: Number(this.iocForm.suspicious_threshold || 3),
  825. },
  826. });
  827. } catch (err) {
  828. this.setErr("IOC evaluate failed", err);
  829. }
  830. },
  831. async loadIocHistory() {
  832. try {
  833. this.ioc.history = await this.apiCall("/ioc/history?limit=50&offset=0");
  834. } catch (err) {
  835. this.setErr("IOC history failed", err);
  836. }
  837. },
  838. async lookupGeoIp() {
  839. try {
  840. const ip = String(this.geoip.ip || "").trim();
  841. if (!ip) {
  842. throw new Error("IP is required");
  843. }
  844. this.geoip.result = await this.apiCall(`/geoip/${encodeURIComponent(ip)}`);
  845. } catch (err) {
  846. this.setErr("GeoIP lookup failed", err);
  847. }
  848. },
  849. onFileSelected(event) {
  850. const files = event && event.target ? event.target.files : null;
  851. this.iocFileForm.file = files && files.length ? files[0] : null;
  852. },
  853. async uploadIocFile() {
  854. try {
  855. if (!this.iocFileForm.file) {
  856. throw new Error("Select a file first");
  857. }
  858. const form = new FormData();
  859. form.append("file", this.iocFileForm.file);
  860. this.ioc.upload = await this.apiCall("/ioc/upload-file", { method: "POST", body: form });
  861. } catch (err) {
  862. this.setErr("IOC upload failed", err);
  863. }
  864. },
  865. async evaluateIocFile() {
  866. try {
  867. if (!this.iocFileForm.file) {
  868. throw new Error("Select a file first");
  869. }
  870. const params = new URLSearchParams({
  871. malicious_threshold: String(this.iocForm.malicious_threshold || 1),
  872. suspicious_threshold: String(this.iocForm.suspicious_threshold || 3),
  873. poll_timeout_seconds: String(this.iocFileForm.poll_timeout_seconds || 30),
  874. poll_interval_seconds: String(this.iocFileForm.poll_interval_seconds || 2),
  875. });
  876. const form = new FormData();
  877. form.append("file", this.iocFileForm.file);
  878. this.ioc.fileEval = await this.apiCall(`/ioc/evaluate-file?${params.toString()}`, { method: "POST", body: form });
  879. } catch (err) {
  880. this.setErr("IOC file evaluate failed", err);
  881. }
  882. },
  883. async getIocAnalysis() {
  884. try {
  885. if (!this.iocFileForm.analysis_id.trim()) {
  886. throw new Error("analysis_id is required");
  887. }
  888. this.ioc.analysis = await this.apiCall(`/ioc/analysis/${encodeURIComponent(this.iocFileForm.analysis_id.trim())}`);
  889. } catch (err) {
  890. this.setErr("IOC analysis failed", err);
  891. }
  892. },
  893. async createIrisTicket() {
  894. try {
  895. this.iris.create = await this.apiCall("/iris/tickets", {
  896. method: "POST",
  897. json: {
  898. title: this.irisForm.title,
  899. description: this.irisForm.description,
  900. case_customer: Number(this.irisForm.case_customer || 1),
  901. case_soc_id: this.irisForm.case_soc_id,
  902. payload: {},
  903. },
  904. });
  905. } catch (err) {
  906. this.setErr("IRIS create failed", err);
  907. }
  908. },
  909. async loadIrisTickets() {
  910. try {
  911. this.iris.list = await this.apiCall(`/iris/tickets?limit=${Number(this.irisList.limit || 50)}&offset=${Number(this.irisList.offset || 0)}`);
  912. } catch (err) {
  913. this.setErr("IRIS list failed", err);
  914. }
  915. },
  916. async loadShuffleHealth() {
  917. try {
  918. this.shuffle.status = await this.apiCall("/shuffle/health");
  919. } catch (err) {
  920. this.setErr("Shuffle health failed", err);
  921. }
  922. },
  923. async loadShuffleAuth() {
  924. try {
  925. this.shuffle.status = await this.apiCall("/shuffle/auth-test");
  926. } catch (err) {
  927. this.setErr("Shuffle auth failed", err);
  928. }
  929. },
  930. async loadShuffleApps() {
  931. try {
  932. this.shuffle.catalog = await this.apiCall("/shuffle/apps");
  933. } catch (err) {
  934. this.setErr("Shuffle apps failed", err);
  935. }
  936. },
  937. async loadShuffleWorkflows() {
  938. try {
  939. this.shuffle.catalog = await this.apiCall("/shuffle/workflows");
  940. } catch (err) {
  941. this.setErr("Shuffle workflows failed", err);
  942. }
  943. },
  944. async executeShuffleWorkflow() {
  945. try {
  946. if (!this.shuffleExec.workflow_id.trim()) {
  947. throw new Error("workflow_id is required");
  948. }
  949. const payload = parseJsonOrThrow(this.shuffleExec.payloadText, "shuffle payload");
  950. this.shuffle.execute = await this.apiCall(`/shuffle/workflows/${encodeURIComponent(this.shuffleExec.workflow_id.trim())}/execute`, {
  951. method: "POST",
  952. json: payload,
  953. });
  954. } catch (err) {
  955. this.setErr("Shuffle execute failed", err);
  956. }
  957. },
  958. async wazuhCall(mode) {
  959. try {
  960. if (mode === "auth") {
  961. this.wazuh.status = await this.apiCall("/wazuh/auth-test");
  962. } else if (mode === "manager") {
  963. this.wazuh.status = await this.apiCall("/wazuh/manager-info");
  964. } else if (mode === "version") {
  965. this.wazuh.status = await this.apiCall("/sync/wazuh-version");
  966. } else if (mode === "autosync") {
  967. this.wazuh.status = await this.apiCall("/wazuh/auto-sync/status");
  968. }
  969. } catch (err) {
  970. this.setErr("Wazuh status failed", err);
  971. }
  972. },
  973. async loadWazuhAgents() {
  974. try {
  975. this.wazuh.list = await this.apiCall(`/wazuh/agents?limit=${Number(this.wazuhList.limit || 50)}&offset=${Number(this.wazuhList.offset || 0)}`);
  976. } catch (err) {
  977. this.setErr("Wazuh agents failed", err);
  978. }
  979. },
  980. async loadWazuhAlerts() {
  981. try {
  982. const q = this.wazuhList.q ? `&q=${encodeURIComponent(this.wazuhList.q)}` : "";
  983. this.wazuh.list = await this.apiCall(`/wazuh/alerts?limit=${Number(this.wazuhList.limit || 50)}&offset=${Number(this.wazuhList.offset || 0)}${q}`);
  984. } catch (err) {
  985. this.setErr("Wazuh alerts failed", err);
  986. }
  987. },
  988. async loadWazuhManagerLogs() {
  989. try {
  990. const q = this.wazuhList.q ? `&q=${encodeURIComponent(this.wazuhList.q)}` : "";
  991. this.wazuh.list = await this.apiCall(`/wazuh/manager-logs?limit=${Number(this.wazuhList.limit || 50)}&offset=${Number(this.wazuhList.offset || 0)}${q}`);
  992. } catch (err) {
  993. this.setErr("Wazuh manager logs failed", err);
  994. }
  995. },
  996. async syncWazuhToMvp() {
  997. try {
  998. const params = new URLSearchParams({
  999. limit: String(Number(this.wazuhSync.limit || 50)),
  1000. minutes: String(Number(this.wazuhSync.minutes || 120)),
  1001. q: String(this.wazuhSync.q || "soc_mvp_test=true OR event_type:*"),
  1002. });
  1003. this.wazuh.sync = await this.apiCall(`/wazuh/sync-to-mvp?${params.toString()}`, {
  1004. method: "POST",
  1005. internal: true,
  1006. });
  1007. } catch (err) {
  1008. this.setErr("Wazuh sync failed", err);
  1009. }
  1010. },
  1011. async loadMvpDependencies() {
  1012. try {
  1013. this.mvp.status = await this.apiCall("/mvp/health/dependencies");
  1014. } catch (err) {
  1015. this.setErr("MVP dependencies failed", err);
  1016. }
  1017. },
  1018. async loadMvpPolicy() {
  1019. try {
  1020. const result = await this.apiCall("/mvp/config/policies");
  1021. this.mvp.status = result;
  1022. const policy = result && result.data ? result.data.policy : null;
  1023. if (policy) {
  1024. this.mvp.policyText = JSON.stringify(policy, null, 2);
  1025. }
  1026. } catch (err) {
  1027. this.setErr("MVP policy get failed", err);
  1028. }
  1029. },
  1030. async updateMvpPolicy() {
  1031. try {
  1032. const payload = parseJsonOrThrow(this.mvp.policyText, "policy JSON");
  1033. this.mvp.status = await this.apiCall("/mvp/config/policies", {
  1034. method: "PUT",
  1035. internal: true,
  1036. json: payload,
  1037. });
  1038. } catch (err) {
  1039. this.setErr("MVP policy update failed", err);
  1040. }
  1041. },
  1042. async mvpIngestIncident() {
  1043. try {
  1044. const payload = parseJsonOrThrow(this.mvp.ingestText, "mvp ingest JSON");
  1045. this.mvp.ingest = await this.apiCall("/mvp/incidents/ingest", {
  1046. method: "POST",
  1047. internal: true,
  1048. json: payload,
  1049. });
  1050. } catch (err) {
  1051. this.setErr("MVP ingest failed", err);
  1052. }
  1053. },
  1054. async mvpEvaluateIoc() {
  1055. try {
  1056. const payload = parseJsonOrThrow(this.mvp.iocEvalText, "mvp IOC JSON");
  1057. this.mvp.evaluate = await this.apiCall("/mvp/ioc/evaluate", {
  1058. method: "POST",
  1059. internal: true,
  1060. json: payload,
  1061. });
  1062. } catch (err) {
  1063. this.setErr("MVP IOC evaluate failed", err);
  1064. }
  1065. },
  1066. async mvpEvaluateVpn() {
  1067. try {
  1068. const payload = parseJsonOrThrow(this.mvp.vpnEvalText, "mvp VPN JSON");
  1069. this.mvp.evaluate = await this.apiCall("/mvp/vpn/evaluate", {
  1070. method: "POST",
  1071. internal: true,
  1072. json: payload,
  1073. });
  1074. } catch (err) {
  1075. this.setErr("MVP VPN evaluate failed", err);
  1076. }
  1077. },
  1078. async loadOpenApiSpec() {
  1079. try {
  1080. const spec = await this.apiCall("/openapi.json");
  1081. this.explorer.spec = spec;
  1082. const endpoints = [];
  1083. const paths = spec && spec.paths ? spec.paths : {};
  1084. Object.keys(paths).forEach((path) => {
  1085. const item = paths[path] || {};
  1086. ["get", "post", "put", "delete", "patch"].forEach((method) => {
  1087. if (item[method]) {
  1088. endpoints.push({
  1089. key: `${method.toUpperCase()} ${path}`,
  1090. method,
  1091. path,
  1092. operation: item[method],
  1093. pathItem: item,
  1094. });
  1095. }
  1096. });
  1097. });
  1098. this.explorer.endpoints = endpoints.sort((a, b) => a.key.localeCompare(b.key));
  1099. if (this.explorer.endpoints.length > 0 && !this.explorer.selectedKey) {
  1100. this.explorer.selectedKey = this.explorer.endpoints[0].key;
  1101. this.selectExplorerEndpoint();
  1102. }
  1103. } catch (err) {
  1104. this.setErr("OpenAPI load failed", err);
  1105. }
  1106. },
  1107. selectExplorerEndpoint() {
  1108. const selected = this.explorer.endpoints.find((x) => x.key === this.explorer.selectedKey) || null;
  1109. this.explorer.selected = selected;
  1110. if (!selected) {
  1111. return;
  1112. }
  1113. const params = []
  1114. .concat(selected.pathItem.parameters || [])
  1115. .concat(selected.operation.parameters || []);
  1116. const pathParams = {};
  1117. const queryParams = {};
  1118. params.forEach((p) => {
  1119. if (!p || !p.name) {
  1120. return;
  1121. }
  1122. if (p.in === "path") {
  1123. pathParams[p.name] = "";
  1124. }
  1125. if (p.in === "query") {
  1126. queryParams[p.name] = "";
  1127. }
  1128. });
  1129. this.explorer.pathParamsText = JSON.stringify(pathParams, null, 2);
  1130. this.explorer.queryText = JSON.stringify(queryParams, null, 2);
  1131. const hasJsonBody =
  1132. selected.operation &&
  1133. selected.operation.requestBody &&
  1134. selected.operation.requestBody.content &&
  1135. selected.operation.requestBody.content["application/json"];
  1136. this.explorer.bodyText = hasJsonBody ? JSON.stringify({}, null, 2) : "{}";
  1137. },
  1138. async runExplorerRequest() {
  1139. try {
  1140. const selected = this.explorer.selected;
  1141. if (!selected) {
  1142. throw new Error("Select an endpoint first");
  1143. }
  1144. const pathParams = parseJsonOrThrow(this.explorer.pathParamsText, "path params");
  1145. const queryParams = parseJsonOrThrow(this.explorer.queryText, "query params");
  1146. const body = parseJsonOrThrow(this.explorer.bodyText, "body");
  1147. let path = selected.path;
  1148. Object.keys(pathParams).forEach((k) => {
  1149. path = path.replace(`{${k}}`, encodeURIComponent(String(pathParams[k])));
  1150. });
  1151. const qs = new URLSearchParams();
  1152. Object.keys(queryParams).forEach((k) => {
  1153. const v = queryParams[k];
  1154. if (v !== "" && v !== null && v !== undefined) {
  1155. qs.append(k, String(v));
  1156. }
  1157. });
  1158. const fullPath = qs.toString() ? `${path}?${qs.toString()}` : path;
  1159. const needsInternal = fullPath.startsWith("/monitor/") || fullPath.startsWith("/mvp/") || fullPath.startsWith("/wazuh/sync-to-mvp");
  1160. const withBody = ["post", "put", "patch"].includes(selected.method);
  1161. this.explorer.result = await this.apiCall(fullPath, {
  1162. method: selected.method.toUpperCase(),
  1163. internal: needsInternal,
  1164. ...(withBody ? { json: body } : {}),
  1165. });
  1166. } catch (err) {
  1167. this.setErr("Explorer request failed", err);
  1168. }
  1169. },
  1170. };
  1171. };