@nano-rs/dac-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,930 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/parser/index.ts
9
+ import { parse as parseYaml } from "yaml";
10
+ var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
11
+ function parseDetectionFile(content, filePath) {
12
+ const errors = [];
13
+ const match = content.match(FRONTMATTER_REGEX);
14
+ if (!match) {
15
+ if (!content.startsWith("---")) {
16
+ return {
17
+ success: false,
18
+ errors: [
19
+ {
20
+ type: "frontmatter_missing",
21
+ message: "Detection file must start with YAML frontmatter delimited by ---",
22
+ line: 1
23
+ }
24
+ ]
25
+ };
26
+ }
27
+ const closingIndex = content.indexOf("\n---", 4);
28
+ if (closingIndex === -1) {
29
+ return {
30
+ success: false,
31
+ errors: [
32
+ {
33
+ type: "frontmatter_malformed",
34
+ message: "Missing closing --- delimiter for frontmatter",
35
+ line: 1
36
+ }
37
+ ]
38
+ };
39
+ }
40
+ return {
41
+ success: false,
42
+ errors: [
43
+ {
44
+ type: "frontmatter_malformed",
45
+ message: "Invalid frontmatter format. Expected: ---\\n<yaml>\\n---\\n<query>"
46
+ }
47
+ ]
48
+ };
49
+ }
50
+ const [, yamlContent, queryContent] = match;
51
+ let metadata;
52
+ try {
53
+ const parsed = parseYaml(yamlContent);
54
+ if (typeof parsed !== "object" || parsed === null) {
55
+ return {
56
+ success: false,
57
+ errors: [
58
+ {
59
+ type: "yaml_invalid",
60
+ message: "Frontmatter must be a YAML object"
61
+ }
62
+ ]
63
+ };
64
+ }
65
+ metadata = parsed;
66
+ } catch (e) {
67
+ const yamlError = e;
68
+ const line = yamlError.linePos?.[0]?.line;
69
+ const col = yamlError.linePos?.[0]?.col;
70
+ return {
71
+ success: false,
72
+ errors: [
73
+ {
74
+ type: "yaml_invalid",
75
+ message: `Invalid YAML: ${yamlError.message}`,
76
+ line: line ? line + 1 : void 0,
77
+ // +1 for the opening ---
78
+ column: col
79
+ }
80
+ ]
81
+ };
82
+ }
83
+ if (typeof metadata.mitre_tactics === "string") {
84
+ metadata.mitre_tactics = [metadata.mitre_tactics];
85
+ }
86
+ if (typeof metadata.mitre_techniques === "string") {
87
+ metadata.mitre_techniques = [metadata.mitre_techniques];
88
+ }
89
+ if (typeof metadata.tags === "string") {
90
+ metadata.tags = [metadata.tags];
91
+ }
92
+ const query = queryContent.trim();
93
+ if (!query) {
94
+ return {
95
+ success: false,
96
+ errors: [
97
+ {
98
+ type: "query_missing",
99
+ message: "Detection file must include a query after the frontmatter"
100
+ }
101
+ ]
102
+ };
103
+ }
104
+ return {
105
+ success: true,
106
+ detection: {
107
+ filePath,
108
+ metadata,
109
+ query,
110
+ raw: content
111
+ },
112
+ errors
113
+ };
114
+ }
115
+ function parseMultipleFiles(files) {
116
+ const results = /* @__PURE__ */ new Map();
117
+ for (const file of files) {
118
+ results.set(file.path, parseDetectionFile(file.content, file.path));
119
+ }
120
+ return results;
121
+ }
122
+ function serializeDetection(detection) {
123
+ const { stringify } = __require("yaml");
124
+ const yaml = stringify(detection.metadata);
125
+ return `---
126
+ ${yaml}---
127
+ ${detection.query}
128
+ `;
129
+ }
130
+
131
+ // src/validator/index.ts
132
+ var VALID_SEVERITIES = [
133
+ "critical",
134
+ "high",
135
+ "medium",
136
+ "low",
137
+ "informational"
138
+ ];
139
+ var VALID_MODES = ["staging", "live", "alerting", "paused"];
140
+ var VALID_DETECTION_MODES = ["real-time", "scheduled"];
141
+ var VALID_CASE_VISIBILITIES = ["public", "group", "private"];
142
+ var VALID_ALERT_MODES = ["grouped", "per_event"];
143
+ var VALID_AUTO_TUNING_MODES = ["off", "proposals", "auto_approve"];
144
+ var CRON_FIELD = "(\\*|\\*\\/\\d+|\\d+|\\d+-\\d+|[\\d,\\-\\/\\*]+)";
145
+ var CRON_5_FIELD = new RegExp(
146
+ `^${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}$`
147
+ );
148
+ var CRON_6_FIELD = new RegExp(
149
+ `^${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}\\s+${CRON_FIELD}$`
150
+ );
151
+ var LOOKBACK_REGEX = /^(\d+)(s|m|h|d)$/;
152
+ var MITRE_TACTIC_REGEX = /^TA\d{4}$/;
153
+ var MITRE_TECHNIQUE_REGEX = /^T\d{4}(\.\d{3})?$/;
154
+ function validateDetection(detection) {
155
+ const errors = [];
156
+ const warnings = [];
157
+ const { metadata, query } = detection;
158
+ validateRequired(metadata, errors);
159
+ validateEnums(metadata, errors, warnings);
160
+ validateSchedule(metadata, errors, warnings);
161
+ validateLookback(metadata, errors);
162
+ validateMitre(metadata, warnings);
163
+ validateAiTriageHints(metadata, warnings);
164
+ validateFolder(metadata, errors);
165
+ validateCaseVisibility(metadata, errors);
166
+ validateAutoTuning(metadata, errors);
167
+ validateRequires(metadata, warnings);
168
+ validateQuery(query, errors, warnings);
169
+ return {
170
+ valid: errors.length === 0,
171
+ errors,
172
+ warnings
173
+ };
174
+ }
175
+ function validateRequired(metadata, errors) {
176
+ if (!metadata.title || metadata.title.trim() === "") {
177
+ errors.push({
178
+ field: "title",
179
+ message: "Title is required",
180
+ code: "REQUIRED_FIELD"
181
+ });
182
+ }
183
+ if (!metadata.severity) {
184
+ errors.push({
185
+ field: "severity",
186
+ message: "Severity is required",
187
+ code: "REQUIRED_FIELD"
188
+ });
189
+ }
190
+ if (!metadata.mode) {
191
+ errors.push({
192
+ field: "mode",
193
+ message: "Mode is required",
194
+ code: "REQUIRED_FIELD"
195
+ });
196
+ }
197
+ if (!metadata.detection_mode) {
198
+ errors.push({
199
+ field: "detection_mode",
200
+ message: "Detection mode is required",
201
+ code: "REQUIRED_FIELD"
202
+ });
203
+ }
204
+ }
205
+ function validateEnums(metadata, errors, warnings) {
206
+ if (metadata.severity && !VALID_SEVERITIES.includes(metadata.severity)) {
207
+ errors.push({
208
+ field: "severity",
209
+ message: `Invalid severity: "${metadata.severity}". Must be one of: ${VALID_SEVERITIES.join(", ")}`,
210
+ code: "INVALID_ENUM"
211
+ });
212
+ }
213
+ if (metadata.mode && !VALID_MODES.includes(metadata.mode)) {
214
+ errors.push({
215
+ field: "mode",
216
+ message: `Invalid mode: "${metadata.mode}". Must be one of: ${VALID_MODES.join(", ")}`,
217
+ code: "INVALID_ENUM"
218
+ });
219
+ }
220
+ if (metadata.detection_mode && !VALID_DETECTION_MODES.includes(
221
+ metadata.detection_mode
222
+ )) {
223
+ errors.push({
224
+ field: "detection_mode",
225
+ message: `Invalid detection_mode: "${metadata.detection_mode}". Must be one of: ${VALID_DETECTION_MODES.join(", ")}`,
226
+ code: "INVALID_ENUM"
227
+ });
228
+ }
229
+ if (metadata.alert_mode && !VALID_ALERT_MODES.includes(
230
+ metadata.alert_mode
231
+ )) {
232
+ errors.push({
233
+ field: "alert_mode",
234
+ message: `Invalid alert_mode: "${metadata.alert_mode}". Must be one of: ${VALID_ALERT_MODES.join(", ")}`,
235
+ code: "INVALID_ENUM"
236
+ });
237
+ }
238
+ if (metadata.alert_mode === "per_event" && metadata.detection_mode === "real-time") {
239
+ warnings.push({
240
+ field: "alert_mode",
241
+ message: "per_event alert_mode with real-time detection_mode is redundant \u2014 real-time rules already process events individually",
242
+ code: "REDUNDANT_CONFIG"
243
+ });
244
+ }
245
+ }
246
+ function validateSchedule(metadata, errors, warnings) {
247
+ if (metadata.detection_mode === "scheduled") {
248
+ if (!metadata.schedule) {
249
+ warnings.push({
250
+ field: "schedule",
251
+ message: "No schedule specified for scheduled detection. Will use default (every minute)",
252
+ code: "MISSING_OPTIONAL"
253
+ });
254
+ } else {
255
+ const isCron5 = CRON_5_FIELD.test(metadata.schedule);
256
+ const isCron6 = CRON_6_FIELD.test(metadata.schedule);
257
+ if (!isCron5 && !isCron6) {
258
+ errors.push({
259
+ field: "schedule",
260
+ message: `Invalid cron expression: "${metadata.schedule}". Expected 5 or 6 field cron format`,
261
+ code: "INVALID_CRON"
262
+ });
263
+ }
264
+ }
265
+ }
266
+ }
267
+ function validateLookback(metadata, errors) {
268
+ if (metadata.lookback && !LOOKBACK_REGEX.test(metadata.lookback)) {
269
+ errors.push({
270
+ field: "lookback",
271
+ message: `Invalid lookback format: "${metadata.lookback}". Expected format like "15m", "1h", "24h", "7d"`,
272
+ code: "INVALID_FORMAT"
273
+ });
274
+ }
275
+ }
276
+ function validateMitre(metadata, warnings) {
277
+ const tactics = Array.isArray(metadata.mitre_tactics) ? metadata.mitre_tactics : metadata.mitre_tactics ? [metadata.mitre_tactics] : [];
278
+ const techniques = Array.isArray(metadata.mitre_techniques) ? metadata.mitre_techniques : metadata.mitre_techniques ? [metadata.mitre_techniques] : [];
279
+ if (tactics.length === 0 && techniques.length === 0) {
280
+ warnings.push({
281
+ field: "mitre",
282
+ message: "No MITRE ATT&CK mappings specified",
283
+ code: "MISSING_MITRE"
284
+ });
285
+ }
286
+ for (const tactic of tactics) {
287
+ if (!MITRE_TACTIC_REGEX.test(tactic)) {
288
+ warnings.push({
289
+ field: "mitre_tactics",
290
+ message: `Non-standard tactic format: "${tactic}" (expected TAxxxx)`,
291
+ code: "INVALID_MITRE_FORMAT"
292
+ });
293
+ }
294
+ }
295
+ for (const technique of techniques) {
296
+ if (!MITRE_TECHNIQUE_REGEX.test(technique)) {
297
+ warnings.push({
298
+ field: "mitre_techniques",
299
+ message: `Non-standard technique format: "${technique}" (expected Txxxx or Txxxx.xxx)`,
300
+ code: "INVALID_MITRE_FORMAT"
301
+ });
302
+ }
303
+ }
304
+ }
305
+ function validateAiTriageHints(metadata, warnings) {
306
+ const hints = metadata.ai_triage_hints;
307
+ if (!hints) return;
308
+ if (hints.ignore_when && hints.ignore_when.length === 0) {
309
+ warnings.push({
310
+ field: "ai_triage_hints.ignore_when",
311
+ message: "ignore_when array is empty. Consider adding benign conditions or removing the field",
312
+ code: "EMPTY_ARRAY"
313
+ });
314
+ }
315
+ if (hints.suspicious_when && hints.suspicious_when.length === 0) {
316
+ warnings.push({
317
+ field: "ai_triage_hints.suspicious_when",
318
+ message: "suspicious_when array is empty. Consider adding suspicious conditions or removing the field",
319
+ code: "EMPTY_ARRAY"
320
+ });
321
+ }
322
+ const allHints = [...hints.ignore_when || [], ...hints.suspicious_when || []];
323
+ for (const hint of allHints) {
324
+ if (hint.length > 200) {
325
+ warnings.push({
326
+ field: "ai_triage_hints",
327
+ message: `Hint is very long (${hint.length} chars). Consider being more concise`,
328
+ code: "HINT_TOO_LONG"
329
+ });
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ var FOLDER_REGEX = /^[a-z0-9][a-z0-9_-]{0,49}$/;
335
+ function validateFolder(metadata, errors) {
336
+ if (metadata.folder === void 0) return;
337
+ if (!FOLDER_REGEX.test(metadata.folder)) {
338
+ errors.push({
339
+ field: "folder",
340
+ message: `Invalid folder: "${metadata.folder}". Must be lowercase alphanumeric with hyphens/underscores, max 50 chars`,
341
+ code: "INVALID_FORMAT"
342
+ });
343
+ }
344
+ }
345
+ function validateCaseVisibility(metadata, errors) {
346
+ if (metadata.case_visibility) {
347
+ if (!VALID_CASE_VISIBILITIES.includes(
348
+ metadata.case_visibility
349
+ )) {
350
+ errors.push({
351
+ field: "case_visibility",
352
+ message: `Invalid case_visibility: "${metadata.case_visibility}". Must be one of: ${VALID_CASE_VISIBILITIES.join(", ")}`,
353
+ code: "INVALID_ENUM"
354
+ });
355
+ }
356
+ if (metadata.case_visibility === "group") {
357
+ if (!metadata.case_groups || metadata.case_groups.length === 0) {
358
+ errors.push({
359
+ field: "case_groups",
360
+ message: 'case_groups is required when case_visibility is "group"',
361
+ code: "REQUIRED_FIELD"
362
+ });
363
+ }
364
+ }
365
+ }
366
+ if (metadata.case_groups) {
367
+ const SLUG_REGEX = /^[a-z0-9-]+$/;
368
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
369
+ for (const group of metadata.case_groups) {
370
+ if (!SLUG_REGEX.test(group) && !UUID_REGEX.test(group)) {
371
+ errors.push({
372
+ field: "case_groups",
373
+ message: `Invalid group identifier: "${group}". Must be lowercase slug or UUID`,
374
+ code: "INVALID_FORMAT"
375
+ });
376
+ }
377
+ }
378
+ }
379
+ }
380
+ function validateAutoTuning(metadata, errors) {
381
+ const autoTuning = metadata.auto_tuning;
382
+ if (!autoTuning) return;
383
+ if (autoTuning.mode && !VALID_AUTO_TUNING_MODES.includes(
384
+ autoTuning.mode
385
+ )) {
386
+ errors.push({
387
+ field: "auto_tuning.mode",
388
+ message: `Invalid auto_tuning mode: "${autoTuning.mode}". Must be one of: ${VALID_AUTO_TUNING_MODES.join(", ")}`,
389
+ code: "INVALID_ENUM"
390
+ });
391
+ }
392
+ if (autoTuning.min_confidence !== void 0) {
393
+ if (typeof autoTuning.min_confidence !== "number" || autoTuning.min_confidence < 0 || autoTuning.min_confidence > 1) {
394
+ errors.push({
395
+ field: "auto_tuning.min_confidence",
396
+ message: `min_confidence must be a number between 0.0 and 1.0. Got: ${autoTuning.min_confidence}`,
397
+ code: "OUT_OF_RANGE"
398
+ });
399
+ }
400
+ }
401
+ if (autoTuning.enabled !== void 0 && typeof autoTuning.enabled !== "boolean") {
402
+ errors.push({
403
+ field: "auto_tuning.enabled",
404
+ message: "auto_tuning.enabled must be a boolean",
405
+ code: "INVALID_TYPE"
406
+ });
407
+ }
408
+ if (autoTuning.critical !== void 0 && typeof autoTuning.critical !== "boolean") {
409
+ errors.push({
410
+ field: "auto_tuning.critical",
411
+ message: "auto_tuning.critical must be a boolean",
412
+ code: "INVALID_TYPE"
413
+ });
414
+ }
415
+ }
416
+ function validateRequires(metadata, warnings) {
417
+ const requires = metadata.requires;
418
+ if (!requires) return;
419
+ if (!requires.fields || requires.fields.length === 0) {
420
+ warnings.push({
421
+ field: "requires.fields",
422
+ message: "requires.fields is empty. Consider specifying required UDM fields",
423
+ code: "EMPTY_ARRAY"
424
+ });
425
+ }
426
+ const FIELD_REGEX = /^[a-z][a-z0-9_]*$/;
427
+ if (requires.fields) {
428
+ for (const field of requires.fields) {
429
+ if (!FIELD_REGEX.test(field)) {
430
+ warnings.push({
431
+ field: "requires.fields",
432
+ message: `Potentially invalid UDM field name: "${field}". Expected lowercase with underscores`,
433
+ code: "INVALID_FORMAT"
434
+ });
435
+ }
436
+ }
437
+ }
438
+ if (requires.source_types) {
439
+ for (const sourceType of requires.source_types) {
440
+ if (!FIELD_REGEX.test(sourceType.toLowerCase())) {
441
+ warnings.push({
442
+ field: "requires.source_types",
443
+ message: `Potentially invalid source_type: "${sourceType}"`,
444
+ code: "INVALID_FORMAT"
445
+ });
446
+ }
447
+ }
448
+ }
449
+ }
450
+ function validateQuery(query, errors, warnings) {
451
+ if (!query || query.trim() === "") {
452
+ errors.push({
453
+ field: "query",
454
+ message: "Query body is required",
455
+ code: "REQUIRED_FIELD"
456
+ });
457
+ return;
458
+ }
459
+ const trimmed = query.trim();
460
+ if (!trimmed.startsWith("source_type=") && !trimmed.startsWith("*")) {
461
+ warnings.push({
462
+ field: "query",
463
+ message: "Query should typically start with source_type= filter or *",
464
+ code: "QUERY_NO_SOURCE"
465
+ });
466
+ }
467
+ const singleQuotes = (trimmed.match(/'/g) || []).length;
468
+ const doubleQuotes = (trimmed.match(/"/g) || []).length;
469
+ if (singleQuotes % 2 !== 0) {
470
+ errors.push({
471
+ field: "query",
472
+ message: "Unclosed single quote in query",
473
+ code: "UNCLOSED_QUOTE"
474
+ });
475
+ }
476
+ if (doubleQuotes % 2 !== 0) {
477
+ errors.push({
478
+ field: "query",
479
+ message: "Unclosed double quote in query",
480
+ code: "UNCLOSED_QUOTE"
481
+ });
482
+ }
483
+ }
484
+ function parseLookbackToMinutes(lookback) {
485
+ const match = lookback.match(LOOKBACK_REGEX);
486
+ if (!match) return void 0;
487
+ const value = parseInt(match[1], 10);
488
+ const unit = match[2];
489
+ switch (unit) {
490
+ case "s":
491
+ return Math.ceil(value / 60);
492
+ case "m":
493
+ return value;
494
+ case "h":
495
+ return value * 60;
496
+ case "d":
497
+ return value * 60 * 24;
498
+ default:
499
+ return void 0;
500
+ }
501
+ }
502
+
503
+ // src/client/index.ts
504
+ var NanosiemClient = class {
505
+ apiUrl;
506
+ searchUrl;
507
+ apiKey;
508
+ timeout;
509
+ constructor(config) {
510
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
511
+ this.searchUrl = config.searchUrl?.replace(/\/$/, "") || this.apiUrl;
512
+ this.apiKey = config.apiKey;
513
+ this.timeout = config.timeout ?? 3e4;
514
+ }
515
+ /**
516
+ * Make an API request
517
+ */
518
+ async request(method, path, body, useSearchUrl = false) {
519
+ const controller = new AbortController();
520
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
521
+ const baseUrl = useSearchUrl ? this.searchUrl : this.apiUrl;
522
+ try {
523
+ const response = await fetch(`${baseUrl}${path}`, {
524
+ method,
525
+ headers: {
526
+ "Content-Type": "application/json",
527
+ "X-API-Key": this.apiKey
528
+ },
529
+ body: body ? JSON.stringify(body) : void 0,
530
+ signal: controller.signal
531
+ });
532
+ clearTimeout(timeoutId);
533
+ if (!response.ok) {
534
+ const errorBody = await response.json().catch(() => ({}));
535
+ return {
536
+ success: false,
537
+ error: {
538
+ code: `HTTP_${response.status}`,
539
+ message: errorBody.error?.message || errorBody.message || response.statusText,
540
+ details: errorBody
541
+ }
542
+ };
543
+ }
544
+ const data = await response.json();
545
+ return { success: true, data };
546
+ } catch (error) {
547
+ clearTimeout(timeoutId);
548
+ if (error instanceof Error && error.name === "AbortError") {
549
+ return {
550
+ success: false,
551
+ error: {
552
+ code: "TIMEOUT",
553
+ message: `Request timed out after ${this.timeout}ms`
554
+ }
555
+ };
556
+ }
557
+ return {
558
+ success: false,
559
+ error: {
560
+ code: "NETWORK_ERROR",
561
+ message: error instanceof Error ? error.message : "Unknown error"
562
+ }
563
+ };
564
+ }
565
+ }
566
+ // ==================== Detection Endpoints ====================
567
+ /**
568
+ * List all detection rules
569
+ */
570
+ async listDetections() {
571
+ return this.request("GET", "/api/rules");
572
+ }
573
+ /**
574
+ * Get a single detection rule by ID
575
+ */
576
+ async getDetection(id) {
577
+ return this.request("GET", `/api/rules/${id}`);
578
+ }
579
+ /**
580
+ * Create a new detection rule
581
+ */
582
+ async createDetection(payload) {
583
+ return this.request("POST", "/api/rules", payload);
584
+ }
585
+ /**
586
+ * Update an existing detection rule
587
+ */
588
+ async updateDetection(id, payload) {
589
+ return this.request(
590
+ "PUT",
591
+ `/api/rules/${id}`,
592
+ payload
593
+ );
594
+ }
595
+ /**
596
+ * Delete a detection rule
597
+ */
598
+ async deleteDetection(id) {
599
+ return this.request(
600
+ "DELETE",
601
+ `/api/rules/${id}`
602
+ );
603
+ }
604
+ /**
605
+ * Pause a detection rule (set mode to paused)
606
+ */
607
+ async pauseDetection(id) {
608
+ return this.request("POST", `/api/rules/${id}/pause`);
609
+ }
610
+ /**
611
+ * Resume a paused detection rule (set mode back to alerting)
612
+ */
613
+ async resumeDetection(id) {
614
+ return this.request("POST", `/api/rules/${id}/resume`);
615
+ }
616
+ /**
617
+ * Test a detection rule against historical data
618
+ */
619
+ async testDetection(id, days) {
620
+ return this.request(
621
+ "POST",
622
+ `/api/rules/${id}/test`,
623
+ { days: days ?? 7 }
624
+ );
625
+ }
626
+ /**
627
+ * Test a query without creating a detection
628
+ */
629
+ async testQuery(query, days) {
630
+ return this.request(
631
+ "POST",
632
+ "/api/rules/test",
633
+ { query, days: days ?? 7 }
634
+ );
635
+ }
636
+ // ==================== Search Endpoints ====================
637
+ /**
638
+ * Execute a search query
639
+ */
640
+ async search(query, timeRange, options) {
641
+ return this.request(
642
+ "POST",
643
+ "/api/search",
644
+ {
645
+ query,
646
+ time_range: timeRange,
647
+ limit: options?.limit,
648
+ include_sql: options?.includeSQL
649
+ },
650
+ true
651
+ // Use search URL
652
+ );
653
+ }
654
+ // ==================== Health Check ====================
655
+ /**
656
+ * Check API health
657
+ */
658
+ async healthCheck() {
659
+ return this.request("GET", "/health");
660
+ }
661
+ // ==================== Rule Lifecycle ====================
662
+ /**
663
+ * Promote a rule (staging -> live -> alerting)
664
+ */
665
+ async promoteDetection(id) {
666
+ return this.request("POST", `/api/rules/${id}/promote`);
667
+ }
668
+ /**
669
+ * Demote a rule (alerting -> live)
670
+ */
671
+ async demoteDetection(id) {
672
+ return this.request("POST", `/api/rules/${id}/demote`);
673
+ }
674
+ /**
675
+ * Manually trigger a rule to execute now
676
+ */
677
+ async triggerDetection(id) {
678
+ return this.request(
679
+ "POST",
680
+ `/api/rules/${id}/trigger`
681
+ );
682
+ }
683
+ // ==================== Query Tools ====================
684
+ /**
685
+ * Validate a query using server-side validation
686
+ */
687
+ async validateQuery(query, detectionMode) {
688
+ return this.request("POST", "/api/rules/validate", {
689
+ query,
690
+ detection_mode: detectionMode
691
+ });
692
+ }
693
+ /**
694
+ * Format a query with pretty-printing
695
+ */
696
+ async formatQuery(query) {
697
+ return this.request("POST", "/api/rules/format", {
698
+ query
699
+ });
700
+ }
701
+ // ==================== Matches & Stats ====================
702
+ /**
703
+ * Get detection matches for a rule
704
+ */
705
+ async getMatches(id, params) {
706
+ const query = new URLSearchParams();
707
+ if (params?.limit) query.set("limit", String(params.limit));
708
+ if (params?.offset) query.set("offset", String(params.offset));
709
+ if (params?.start_time) query.set("start_time", params.start_time);
710
+ if (params?.end_time) query.set("end_time", params.end_time);
711
+ const qs = query.toString();
712
+ return this.request(
713
+ "GET",
714
+ `/api/rules/${id}/matches${qs ? `?${qs}` : ""}`
715
+ );
716
+ }
717
+ /**
718
+ * Get detection stats (match counts by day)
719
+ */
720
+ async getStats(days) {
721
+ const qs = days ? `?days=${days}` : "";
722
+ return this.request("GET", `/api/rules/stats${qs}`);
723
+ }
724
+ // ==================== Bulk Operations ====================
725
+ /**
726
+ * Bulk update rule modes
727
+ */
728
+ async bulkUpdate(ruleIds, mode) {
729
+ return this.request("POST", "/api/rules/bulk-update", {
730
+ rule_ids: ruleIds,
731
+ mode
732
+ });
733
+ }
734
+ /**
735
+ * Import rules in bulk
736
+ */
737
+ async importRules(rules) {
738
+ return this.request("POST", "/api/rules/import", {
739
+ rules
740
+ });
741
+ }
742
+ /**
743
+ * Export all rules
744
+ */
745
+ async exportRules() {
746
+ return this.request("GET", "/api/rules/export");
747
+ }
748
+ };
749
+ function detectionToApiPayload(detection) {
750
+ const { metadata, query } = detection;
751
+ let lookbackMinutes;
752
+ if (metadata.lookback) {
753
+ lookbackMinutes = parseLookbackToMinutes(metadata.lookback);
754
+ }
755
+ const mitreTactics = Array.isArray(metadata.mitre_tactics) ? metadata.mitre_tactics : metadata.mitre_tactics ? [metadata.mitre_tactics] : [];
756
+ const mitreTechniques = Array.isArray(metadata.mitre_techniques) ? metadata.mitre_techniques : metadata.mitre_techniques ? [metadata.mitre_techniques] : [];
757
+ return {
758
+ name: metadata.title,
759
+ description: metadata.description,
760
+ query,
761
+ severity: metadata.severity,
762
+ mode: metadata.mode,
763
+ detection_mode: metadata.detection_mode,
764
+ schedule_cron: metadata.schedule,
765
+ lookback_minutes: lookbackMinutes,
766
+ mitre_tactics: mitreTactics,
767
+ mitre_techniques: mitreTechniques,
768
+ tags: metadata.tags ?? [],
769
+ reference_url: metadata.reference_url,
770
+ author: metadata.author,
771
+ realtime_enabled: metadata.realtime_enabled,
772
+ ai_triage_hints: metadata.ai_triage_hints,
773
+ alert_mode: metadata.alert_mode,
774
+ folder: metadata.folder,
775
+ case_visibility: metadata.case_visibility,
776
+ case_group_ids: metadata.case_groups,
777
+ // Flatten auto_tuning for API (server expects flat fields)
778
+ auto_tuning_enabled: metadata.auto_tuning?.enabled,
779
+ auto_tuning_min_confidence: metadata.auto_tuning?.min_confidence,
780
+ auto_tuning_critical: metadata.auto_tuning?.critical,
781
+ // Playbook auto-attach at firing time. Server enforces that
782
+ // playbook_id is set iff selector_mode is 'specific'. Accepts either
783
+ // the two-field form (`playbook_selector_mode` + `playbook_id`) or the
784
+ // NAN-453 shorthand (`playbook:`). Shorthand wins when both are set.
785
+ ...decodePlaybook(metadata)
786
+ };
787
+ }
788
+ function decodePlaybook(metadata) {
789
+ const shorthand = metadata.playbook?.trim();
790
+ if (shorthand !== void 0) {
791
+ if (shorthand === "" || shorthand === "none") {
792
+ return { playbook_selector_mode: "none", playbook_id: null };
793
+ }
794
+ if (shorthand === "adaptive") {
795
+ return { playbook_selector_mode: "adaptive", playbook_id: null };
796
+ }
797
+ if (/^pb_[0-9a-z]+$/i.test(shorthand)) {
798
+ return { playbook_selector_mode: "specific", playbook_id: shorthand };
799
+ }
800
+ }
801
+ return {
802
+ playbook_selector_mode: metadata.playbook_selector_mode,
803
+ playbook_id: metadata.playbook_selector_mode === "specific" ? metadata.playbook_id ?? null : null
804
+ };
805
+ }
806
+ function detectionRuleToDetection(rule, filePath) {
807
+ let lookback;
808
+ if (rule.lookback_minutes) {
809
+ if (rule.lookback_minutes >= 1440 && rule.lookback_minutes % 1440 === 0) {
810
+ lookback = `${rule.lookback_minutes / 1440}d`;
811
+ } else if (rule.lookback_minutes >= 60 && rule.lookback_minutes % 60 === 0) {
812
+ lookback = `${rule.lookback_minutes / 60}h`;
813
+ } else {
814
+ lookback = `${rule.lookback_minutes}m`;
815
+ }
816
+ }
817
+ const metadata = {
818
+ title: rule.name,
819
+ description: rule.description,
820
+ author: rule.author,
821
+ severity: rule.severity,
822
+ mode: rule.mode,
823
+ detection_mode: rule.detection_mode,
824
+ schedule: rule.schedule_cron,
825
+ lookback,
826
+ mitre_tactics: rule.mitre_tactics,
827
+ mitre_techniques: rule.mitre_techniques,
828
+ tags: rule.tags,
829
+ reference_url: rule.reference_url,
830
+ realtime_enabled: rule.realtime_enabled,
831
+ ai_triage_hints: rule.ai_triage_hints,
832
+ alert_mode: rule.alert_mode,
833
+ folder: rule.folder,
834
+ case_visibility: rule.case_visibility,
835
+ case_groups: rule.case_group_ids,
836
+ // Reconstruct nested auto_tuning from flat API fields
837
+ auto_tuning: rule.auto_tuning_enabled !== void 0 || rule.auto_tuning_min_confidence !== void 0 || rule.auto_tuning_critical !== void 0 ? {
838
+ enabled: rule.auto_tuning_enabled,
839
+ min_confidence: rule.auto_tuning_min_confidence,
840
+ critical: rule.auto_tuning_critical
841
+ } : void 0
842
+ };
843
+ return {
844
+ filePath,
845
+ metadata,
846
+ query: rule.query,
847
+ raw: ""
848
+ // Not available from API
849
+ };
850
+ }
851
+
852
+ // src/grouping/index.ts
853
+ var ENTITY_FIELDS_PRIORITY = [
854
+ "src_ip",
855
+ "dest_ip",
856
+ "user",
857
+ "src_user",
858
+ "dest_user",
859
+ "src_host",
860
+ "dest_host",
861
+ "host",
862
+ "hostname",
863
+ "process_name",
864
+ "file_hash",
865
+ "process_hash"
866
+ ];
867
+ function getDominantEntity(events) {
868
+ for (const field of ENTITY_FIELDS_PRIORITY) {
869
+ const counts = /* @__PURE__ */ new Map();
870
+ for (const event of events) {
871
+ const val = event[field];
872
+ if (val !== void 0 && val !== null && val !== "") {
873
+ const key = String(val);
874
+ counts.set(key, (counts.get(key) || 0) + 1);
875
+ }
876
+ }
877
+ if (counts.size > 0) {
878
+ let maxVal = "";
879
+ let maxCount = 0;
880
+ for (const [val, count] of counts) {
881
+ if (count > maxCount) {
882
+ maxVal = val;
883
+ maxCount = count;
884
+ }
885
+ }
886
+ return {
887
+ field,
888
+ value: maxVal,
889
+ others_count: counts.size - 1
890
+ };
891
+ }
892
+ }
893
+ return null;
894
+ }
895
+ function bucketEventsByLookback(events, lookbackMinutes) {
896
+ if (events.length === 0 || lookbackMinutes <= 0) return [];
897
+ const windowMs = lookbackMinutes * 60 * 1e3;
898
+ const bucketMap = /* @__PURE__ */ new Map();
899
+ for (const event of events) {
900
+ const ts = event.timestamp || event.ingest_time || event._nano_detected_at;
901
+ if (!ts) continue;
902
+ const epochMs = new Date(String(ts)).getTime();
903
+ if (isNaN(epochMs)) continue;
904
+ const windowStart = Math.floor(epochMs / windowMs) * windowMs;
905
+ if (!bucketMap.has(windowStart)) bucketMap.set(windowStart, []);
906
+ bucketMap.get(windowStart).push(event);
907
+ }
908
+ const sorted = [...bucketMap.entries()].sort((a, b) => b[0] - a[0]);
909
+ return sorted.map(([windowStartMs, bucketEvents]) => ({
910
+ window_start: new Date(windowStartMs).toISOString(),
911
+ window_end: new Date(windowStartMs + windowMs).toISOString(),
912
+ event_count: bucketEvents.length,
913
+ dominant_entity: getDominantEntity(bucketEvents),
914
+ sample_events: bucketEvents.slice(0, 3)
915
+ }));
916
+ }
917
+ export {
918
+ ENTITY_FIELDS_PRIORITY,
919
+ NanosiemClient,
920
+ bucketEventsByLookback,
921
+ detectionRuleToDetection,
922
+ detectionToApiPayload,
923
+ getDominantEntity,
924
+ parseDetectionFile,
925
+ parseLookbackToMinutes,
926
+ parseMultipleFiles,
927
+ serializeDetection,
928
+ validateDetection
929
+ };
930
+ //# sourceMappingURL=index.js.map