-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathsplunk.ts
More file actions
171 lines (146 loc) · 5.61 KB
/
Copy pathsplunk.ts
File metadata and controls
171 lines (146 loc) · 5.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
/**
* ATR-to-Splunk SPL Converter
*
* Converts ATR YAML rules into Splunk Search Processing Language (SPL) queries
* that a SOC analyst can use as a starting point for threat hunting.
*
* @module agent-threat-rules/converters/splunk
*/
import type { ATRRule, ATRArrayCondition } from '../types.js';
/**
* Escape a string for use in Splunk SPL double-quoted values.
*/
function escapeForSPL(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
/**
* Convert a single ATR array condition to an SPL clause.
*
* Supports operators: regex, contains, exact, starts_with, gt, lt, gte, lte, eq
*/
function conditionToSPL(cond: ATRArrayCondition): string {
const field = cond.field;
const value = cond.value;
switch (cond.operator) {
case 'regex':
return `| regex ${field}="${escapeForSPL(value)}"`;
case 'contains':
return `${field}="*${escapeForSPL(value)}*"`;
case 'exact':
return `${field}="${escapeForSPL(value)}"`;
case 'starts_with':
return `${field}="${escapeForSPL(value)}*"`;
case 'gt':
return `| where ${field} > ${Number(value)}`;
case 'lt':
return `| where ${field} < ${Number(value)}`;
case 'gte':
return `| where ${field} >= ${Number(value)}`;
case 'lte':
return `| where ${field} <= ${Number(value)}`;
case 'eq':
return `| where ${field} == ${Number(value)}`;
default:
// Fallback: treat unknown operators as a contains search
return `${field}="*${escapeForSPL(value)}*"`;
}
}
/**
* Convert an ATR rule to a Splunk SPL query string.
*
* The generated query includes:
* - Comment header with rule metadata
* - Index/sourcetype base search (generic, analyst should customize)
* - Condition clauses joined with appropriate logic
*/
export function ruleToSPL(rule: ATRRule): string {
const conditions = rule.detection.conditions;
const logic = rule.detection.condition; // "any" or "all"
const lines: string[] = [];
// Comment header with rule metadata
lines.push(`\`\`\` ATR Rule: ${rule.id} \`\`\``);
lines.push(`\`\`\` Title: ${rule.title} \`\`\``);
lines.push(`\`\`\` Severity: ${rule.severity} | Category: ${rule.tags.category} \`\`\``);
lines.push(`\`\`\` Source: ${rule.agent_source.type} | Condition logic: ${logic} \`\`\``);
lines.push('');
// Base search -- analyst should adjust index and sourcetype
lines.push('index=ai_agent_logs sourcetype=agent_events');
if (!Array.isArray(conditions)) {
// Named-map format: not common in current rules, emit a placeholder
lines.push('```` Warning: Named-map conditions not fully supported. Review manually. ````');
return lines.join('\n');
}
const arrayConditions = conditions as ATRArrayCondition[];
if (arrayConditions.length === 0) {
return lines.join('\n');
}
// For "any" logic with regex conditions, we can combine them using
// a single regex with OR (|) alternation where possible, or use
// multiple search branches.
// For clarity and analyst usability, we emit each condition separately.
if (logic === 'all') {
// AND logic: chain all conditions sequentially
for (const cond of arrayConditions) {
lines.push(conditionToSPL(cond));
}
} else {
// OR logic ("any"): use Splunk's multisearch or OR-joined search
// For regex conditions, wrap in a single eval+match approach
// For simplicity and readability, use OR-joined subsearches
const regexConditions = arrayConditions.filter(c => c.operator === 'regex');
const otherConditions = arrayConditions.filter(c => c.operator !== 'regex');
if (regexConditions.length > 0 && otherConditions.length === 0) {
// All regex: combine with OR in eval/match
lines.push('| where (');
const regexClauses = regexConditions.map((cond, i) => {
const prefix = i === 0 ? ' ' : ' OR ';
return `${prefix}match(${cond.field}, "${escapeForSPL(cond.value)}")`;
});
lines.push(...regexClauses);
lines.push(')');
} else {
// Mixed operators: emit each as separate OR clause
lines.push('| where (');
const clauses: string[] = [];
for (const cond of arrayConditions) {
switch (cond.operator) {
case 'regex':
clauses.push(`match(${cond.field}, "${escapeForSPL(cond.value)}")`);
break;
case 'contains':
clauses.push(`like(${cond.field}, "%${escapeForSPL(cond.value)}%")`);
break;
case 'exact':
clauses.push(`${cond.field}="${escapeForSPL(cond.value)}"`);
break;
case 'starts_with':
clauses.push(`like(${cond.field}, "${escapeForSPL(cond.value)}%")`);
break;
case 'gt':
clauses.push(`${cond.field} > ${Number(cond.value)}`);
break;
case 'lt':
clauses.push(`${cond.field} < ${Number(cond.value)}`);
break;
case 'gte':
clauses.push(`${cond.field} >= ${Number(cond.value)}`);
break;
case 'lte':
clauses.push(`${cond.field} <= ${Number(cond.value)}`);
break;
case 'eq':
clauses.push(`${cond.field} == ${Number(cond.value)}`);
break;
default:
clauses.push(`like(${cond.field}, "%${escapeForSPL(cond.value)}%")`);
}
}
lines.push(clauses.map((c, i) => (i === 0 ? ` ${c}` : ` OR ${c}`)).join('\n'));
lines.push(')');
}
}
// Add a table output for the analyst
const fields = [...new Set(arrayConditions.map(c => c.field))];
lines.push(`| table _time ${fields.join(' ')} source`);
return lines.join('\n');
}