説明なし

app.js 34KB

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