tracecat-mcp-community 1.0.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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +267 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +147 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +32 -0
- package/dist/minimal.d.ts +1 -0
- package/dist/minimal.js +8 -0
- package/dist/minimal.js.backup +8 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +32 -0
- package/dist/tools/actions.d.ts +3 -0
- package/dist/tools/actions.js +54 -0
- package/dist/tools/cases.d.ts +3 -0
- package/dist/tools/cases.js +73 -0
- package/dist/tools/docs.d.ts +3 -0
- package/dist/tools/docs.js +389 -0
- package/dist/tools/executions.d.ts +3 -0
- package/dist/tools/executions.js +40 -0
- package/dist/tools/graph.d.ts +3 -0
- package/dist/tools/graph.js +82 -0
- package/dist/tools/schedules.d.ts +3 -0
- package/dist/tools/schedules.js +52 -0
- package/dist/tools/secrets.d.ts +3 -0
- package/dist/tools/secrets.js +56 -0
- package/dist/tools/system.d.ts +3 -0
- package/dist/tools/system.js +16 -0
- package/dist/tools/tables.d.ts +3 -0
- package/dist/tools/tables.js +102 -0
- package/dist/tools/templates.d.ts +3 -0
- package/dist/tools/templates.js +547 -0
- package/dist/tools/webhooks.d.ts +3 -0
- package/dist/tools/webhooks.js +9 -0
- package/dist/tools/workflows.d.ts +3 -0
- package/dist/tools/workflows.js +318 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// Templates from official Tracecat documentation and community sources.
|
|
3
|
+
// To add a template: push a new Template object to this array.
|
|
4
|
+
const TEMPLATES = [
|
|
5
|
+
{
|
|
6
|
+
id: "crowdsec_false_positive",
|
|
7
|
+
title: "CrowdSec False Positive Manager",
|
|
8
|
+
description: "Check IP reputation via CrowdSec CTI API and conditionally unblock benign IPs. Source: aukfood.fr community blog.",
|
|
9
|
+
category: "threat_response",
|
|
10
|
+
actions: 3,
|
|
11
|
+
yaml: `# CrowdSec False Positive Manager
|
|
12
|
+
# Source: https://www.aukfood.fr/faux-positifs-sous-controle-grace-a-tracecat-et-crowdsec/
|
|
13
|
+
# Trigger: webhook with { "ip_address": "x.x.x.x" }
|
|
14
|
+
# Secrets required: crowdsec (keys: API_TOKEN)
|
|
15
|
+
|
|
16
|
+
definition:
|
|
17
|
+
title: CrowdSec False Positive Manager
|
|
18
|
+
description: Check IP reputation via CrowdSec CTI and conditionally unblock benign IPs
|
|
19
|
+
|
|
20
|
+
triggers:
|
|
21
|
+
- type: webhook
|
|
22
|
+
ref: incoming_ip
|
|
23
|
+
|
|
24
|
+
actions:
|
|
25
|
+
- ref: lookup_ip_reputation
|
|
26
|
+
action: core.http_request
|
|
27
|
+
args:
|
|
28
|
+
url: "https://cti.api.crowdsec.net/v2/smoke/\${{ TRIGGER.data.ip_address }}"
|
|
29
|
+
method: GET
|
|
30
|
+
headers:
|
|
31
|
+
x-api-key: \${{ SECRETS.crowdsec.API_TOKEN }}
|
|
32
|
+
Accept: application/json
|
|
33
|
+
|
|
34
|
+
- ref: evaluate_reputation
|
|
35
|
+
action: core.transform.reshape
|
|
36
|
+
args:
|
|
37
|
+
value:
|
|
38
|
+
ip: \${{ TRIGGER.data.ip_address }}
|
|
39
|
+
is_benign: \${{ ACTIONS.lookup_ip_reputation.result.reputation == "benign" }}
|
|
40
|
+
reputation: \${{ ACTIONS.lookup_ip_reputation.result.reputation }}
|
|
41
|
+
total_reports: \${{ ACTIONS.lookup_ip_reputation.result.ip_range_score }}
|
|
42
|
+
background_noise_score: \${{ ACTIONS.lookup_ip_reputation.result.background_noise_score }}
|
|
43
|
+
depends_on: [lookup_ip_reputation]
|
|
44
|
+
|
|
45
|
+
- ref: unblock_benign_ip
|
|
46
|
+
action: core.http_request
|
|
47
|
+
args:
|
|
48
|
+
url: "https://admin.api.crowdsec.net/v1/decisions"
|
|
49
|
+
method: DELETE
|
|
50
|
+
headers:
|
|
51
|
+
x-api-key: \${{ SECRETS.crowdsec.API_TOKEN }}
|
|
52
|
+
Content-Type: application/json
|
|
53
|
+
params:
|
|
54
|
+
ip: \${{ TRIGGER.data.ip_address }}
|
|
55
|
+
run_if: \${{ ACTIONS.evaluate_reputation.result.is_benign }}
|
|
56
|
+
depends_on: [evaluate_reputation]`,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "wazuh_alert_to_case",
|
|
60
|
+
title: "Wazuh Alert-to-Case Pipeline",
|
|
61
|
+
description: "Receive Wazuh alerts via webhook, enrich with agent info, evaluate severity, and create a Tracecat case. Source: aukfood.fr community blog.",
|
|
62
|
+
category: "alert_management",
|
|
63
|
+
actions: 5,
|
|
64
|
+
yaml: `# Wazuh Alert-to-Case Pipeline
|
|
65
|
+
# Source: https://www.aukfood.fr/tracecat-le-soar-open-source-qui-change-la-donne/
|
|
66
|
+
# Trigger: webhook from Wazuh (alert JSON)
|
|
67
|
+
# Secrets required: wazuh (keys: API_KEY, SERVER_URL)
|
|
68
|
+
|
|
69
|
+
definition:
|
|
70
|
+
title: Wazuh Alert-to-Case Pipeline
|
|
71
|
+
description: Ingest Wazuh alerts, enrich with agent info, and create investigation cases
|
|
72
|
+
|
|
73
|
+
triggers:
|
|
74
|
+
- type: webhook
|
|
75
|
+
ref: wazuh_alert
|
|
76
|
+
|
|
77
|
+
actions:
|
|
78
|
+
- ref: parse_alert
|
|
79
|
+
action: core.transform.reshape
|
|
80
|
+
args:
|
|
81
|
+
value:
|
|
82
|
+
alert_id: \${{ TRIGGER.data.id }}
|
|
83
|
+
rule_id: \${{ TRIGGER.data.rule.id }}
|
|
84
|
+
rule_level: \${{ TRIGGER.data.rule.level }}
|
|
85
|
+
rule_description: \${{ TRIGGER.data.rule.description }}
|
|
86
|
+
agent_id: \${{ TRIGGER.data.agent.id }}
|
|
87
|
+
agent_name: \${{ TRIGGER.data.agent.name }}
|
|
88
|
+
timestamp: \${{ TRIGGER.data.timestamp }}
|
|
89
|
+
src_ip: \${{ TRIGGER.data.data.srcip }}
|
|
90
|
+
|
|
91
|
+
- ref: get_agent_info
|
|
92
|
+
action: core.http_request
|
|
93
|
+
args:
|
|
94
|
+
url: "\${{ SECRETS.wazuh.SERVER_URL }}/agents/\${{ ACTIONS.parse_alert.result.agent_id }}"
|
|
95
|
+
method: GET
|
|
96
|
+
headers:
|
|
97
|
+
Authorization: "Bearer \${{ SECRETS.wazuh.API_KEY }}"
|
|
98
|
+
verify_ssl: false
|
|
99
|
+
depends_on: [parse_alert]
|
|
100
|
+
|
|
101
|
+
- ref: evaluate_severity
|
|
102
|
+
action: core.transform.reshape
|
|
103
|
+
args:
|
|
104
|
+
value:
|
|
105
|
+
priority: \${{ FN.conditional(ACTIONS.parse_alert.result.rule_level > 12, "critical", FN.conditional(ACTIONS.parse_alert.result.rule_level > 8, "high", FN.conditional(ACTIONS.parse_alert.result.rule_level > 4, "medium", "low"))) }}
|
|
106
|
+
malice: \${{ FN.conditional(ACTIONS.parse_alert.result.rule_level > 10, "malicious", "unknown") }}
|
|
107
|
+
depends_on: [parse_alert]
|
|
108
|
+
|
|
109
|
+
- ref: create_investigation_case
|
|
110
|
+
action: core.cases.create_case
|
|
111
|
+
args:
|
|
112
|
+
case_title: "[Wazuh] \${{ ACTIONS.parse_alert.result.rule_description }}"
|
|
113
|
+
priority: \${{ ACTIONS.evaluate_severity.result.priority }}
|
|
114
|
+
malice: \${{ ACTIONS.evaluate_severity.result.malice }}
|
|
115
|
+
status: new
|
|
116
|
+
payload:
|
|
117
|
+
alert_id: \${{ ACTIONS.parse_alert.result.alert_id }}
|
|
118
|
+
rule_id: \${{ ACTIONS.parse_alert.result.rule_id }}
|
|
119
|
+
agent: \${{ ACTIONS.get_agent_info.result.data.name }}
|
|
120
|
+
src_ip: \${{ ACTIONS.parse_alert.result.src_ip }}
|
|
121
|
+
depends_on: [evaluate_severity, get_agent_info]
|
|
122
|
+
|
|
123
|
+
- ref: add_context_comment
|
|
124
|
+
action: core.cases.create_comment
|
|
125
|
+
args:
|
|
126
|
+
case_id: \${{ ACTIONS.create_investigation_case.result.id }}
|
|
127
|
+
content: "Auto-created from Wazuh alert. Agent: \${{ ACTIONS.get_agent_info.result.data.name }} (OS: \${{ ACTIONS.get_agent_info.result.data.os.platform }}). Rule level: \${{ ACTIONS.parse_alert.result.rule_level }}"
|
|
128
|
+
depends_on: [create_investigation_case]`,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "crowdstrike_forensics",
|
|
132
|
+
title: "CrowdStrike Evidence Collection",
|
|
133
|
+
description: "Collect forensic evidence from CrowdStrike Falcon: detections, device info, IOCs, and network connections. Source: stickyricebytes.com community blog.",
|
|
134
|
+
category: "forensics",
|
|
135
|
+
actions: 6,
|
|
136
|
+
yaml: `# CrowdStrike Evidence Collection
|
|
137
|
+
# Source: https://stickyricebytes.com/2025/02/11/crowdstrike-falcon-forensics-with-tracecat/
|
|
138
|
+
# Trigger: webhook with { "host_id": "device-id", "detection_id": "ldt:xxx" }
|
|
139
|
+
# Secrets required: crowdstrike (keys: CLIENT_ID, CLIENT_SECRET, BASE_URL)
|
|
140
|
+
|
|
141
|
+
definition:
|
|
142
|
+
title: CrowdStrike Evidence Collection
|
|
143
|
+
description: Gather forensic evidence from CrowdStrike Falcon for incident investigation
|
|
144
|
+
|
|
145
|
+
triggers:
|
|
146
|
+
- type: webhook
|
|
147
|
+
ref: forensic_request
|
|
148
|
+
|
|
149
|
+
actions:
|
|
150
|
+
- ref: authenticate
|
|
151
|
+
action: core.http_request
|
|
152
|
+
args:
|
|
153
|
+
url: \${{ SECRETS.crowdstrike.BASE_URL }}/oauth2/token
|
|
154
|
+
method: POST
|
|
155
|
+
headers:
|
|
156
|
+
Content-Type: application/x-www-form-urlencoded
|
|
157
|
+
payload:
|
|
158
|
+
client_id: \${{ SECRETS.crowdstrike.CLIENT_ID }}
|
|
159
|
+
client_secret: \${{ SECRETS.crowdstrike.CLIENT_SECRET }}
|
|
160
|
+
|
|
161
|
+
- ref: get_detection_details
|
|
162
|
+
action: core.http_request
|
|
163
|
+
args:
|
|
164
|
+
url: \${{ SECRETS.crowdstrike.BASE_URL }}/detects/entities/summaries/GET/v1
|
|
165
|
+
method: POST
|
|
166
|
+
headers:
|
|
167
|
+
Authorization: "Bearer \${{ ACTIONS.authenticate.result.access_token }}"
|
|
168
|
+
Content-Type: application/json
|
|
169
|
+
payload:
|
|
170
|
+
ids:
|
|
171
|
+
- \${{ TRIGGER.data.detection_id }}
|
|
172
|
+
depends_on: [authenticate]
|
|
173
|
+
|
|
174
|
+
- ref: get_device_info
|
|
175
|
+
action: core.http_request
|
|
176
|
+
args:
|
|
177
|
+
url: \${{ SECRETS.crowdstrike.BASE_URL }}/devices/entities/devices/v2
|
|
178
|
+
method: GET
|
|
179
|
+
headers:
|
|
180
|
+
Authorization: "Bearer \${{ ACTIONS.authenticate.result.access_token }}"
|
|
181
|
+
params:
|
|
182
|
+
ids: \${{ TRIGGER.data.host_id }}
|
|
183
|
+
depends_on: [authenticate]
|
|
184
|
+
|
|
185
|
+
- ref: get_iocs
|
|
186
|
+
action: core.http_request
|
|
187
|
+
args:
|
|
188
|
+
url: \${{ SECRETS.crowdstrike.BASE_URL }}/indicators/queries/iocs/v1
|
|
189
|
+
method: GET
|
|
190
|
+
headers:
|
|
191
|
+
Authorization: "Bearer \${{ ACTIONS.authenticate.result.access_token }}"
|
|
192
|
+
params:
|
|
193
|
+
filter: "device.device_id:'\${{ TRIGGER.data.host_id }}'"
|
|
194
|
+
limit: "50"
|
|
195
|
+
depends_on: [authenticate]
|
|
196
|
+
|
|
197
|
+
- ref: get_network_connections
|
|
198
|
+
action: core.http_request
|
|
199
|
+
args:
|
|
200
|
+
url: \${{ SECRETS.crowdstrike.BASE_URL }}/detects/entities/processes/v1
|
|
201
|
+
method: GET
|
|
202
|
+
headers:
|
|
203
|
+
Authorization: "Bearer \${{ ACTIONS.authenticate.result.access_token }}"
|
|
204
|
+
params:
|
|
205
|
+
ids: \${{ ACTIONS.get_detection_details.result.resources[0].behaviors[0].process_id }}
|
|
206
|
+
depends_on: [get_detection_details]
|
|
207
|
+
|
|
208
|
+
- ref: compile_evidence
|
|
209
|
+
action: core.transform.reshape
|
|
210
|
+
args:
|
|
211
|
+
value:
|
|
212
|
+
case_summary:
|
|
213
|
+
detection: \${{ ACTIONS.get_detection_details.result.resources[0] }}
|
|
214
|
+
device:
|
|
215
|
+
hostname: \${{ ACTIONS.get_device_info.result.resources[0].hostname }}
|
|
216
|
+
os: \${{ ACTIONS.get_device_info.result.resources[0].os_version }}
|
|
217
|
+
last_seen: \${{ ACTIONS.get_device_info.result.resources[0].last_seen }}
|
|
218
|
+
external_ip: \${{ ACTIONS.get_device_info.result.resources[0].external_ip }}
|
|
219
|
+
iocs: \${{ ACTIONS.get_iocs.result.resources }}
|
|
220
|
+
network: \${{ ACTIONS.get_network_connections.result }}
|
|
221
|
+
collected_at: \${{ FN.now() }}
|
|
222
|
+
depends_on: [get_detection_details, get_device_info, get_iocs, get_network_connections]`,
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "virustotal_url_enrichment",
|
|
226
|
+
title: "VirusTotal URL Enrichment",
|
|
227
|
+
description: "Submit a URL to VirusTotal for scanning, retrieve analysis results, and evaluate verdict. Source: Tracecat official docs + Discord community.",
|
|
228
|
+
category: "enrichment",
|
|
229
|
+
actions: 3,
|
|
230
|
+
yaml: `# VirusTotal URL Enrichment
|
|
231
|
+
# Source: Tracecat official documentation + Discord community patterns
|
|
232
|
+
# Trigger: webhook with { "url": "https://suspicious-site.com" }
|
|
233
|
+
# Secrets required: virustotal (keys: API_KEY)
|
|
234
|
+
# Note: VT URL lookup requires base64url encoding of the URL
|
|
235
|
+
|
|
236
|
+
definition:
|
|
237
|
+
title: VirusTotal URL Enrichment
|
|
238
|
+
description: Scan a URL with VirusTotal and return analysis verdict
|
|
239
|
+
|
|
240
|
+
triggers:
|
|
241
|
+
- type: webhook
|
|
242
|
+
ref: url_to_scan
|
|
243
|
+
|
|
244
|
+
actions:
|
|
245
|
+
- ref: encode_and_lookup
|
|
246
|
+
action: core.http_request
|
|
247
|
+
args:
|
|
248
|
+
url: "https://www.virustotal.com/api/v3/urls/\${{ FN.strip(FN.to_base64url(TRIGGER.data.url)) }}"
|
|
249
|
+
method: GET
|
|
250
|
+
headers:
|
|
251
|
+
x-apikey: \${{ SECRETS.virustotal.API_KEY }}
|
|
252
|
+
Accept: application/json
|
|
253
|
+
|
|
254
|
+
- ref: extract_verdict
|
|
255
|
+
action: core.transform.reshape
|
|
256
|
+
args:
|
|
257
|
+
value:
|
|
258
|
+
url: \${{ TRIGGER.data.url }}
|
|
259
|
+
malicious: \${{ ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.malicious }}
|
|
260
|
+
suspicious: \${{ ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.suspicious }}
|
|
261
|
+
harmless: \${{ ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.harmless }}
|
|
262
|
+
total: \${{ ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.malicious + ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.suspicious + ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.harmless }}
|
|
263
|
+
categories: \${{ ACTIONS.encode_and_lookup.result.data.attributes.categories }}
|
|
264
|
+
is_malicious: \${{ ACTIONS.encode_and_lookup.result.data.attributes.last_analysis_stats.malicious > 3 }}
|
|
265
|
+
depends_on: [encode_and_lookup]
|
|
266
|
+
|
|
267
|
+
- ref: create_alert_case
|
|
268
|
+
action: core.cases.create_case
|
|
269
|
+
args:
|
|
270
|
+
case_title: "[VT URL] Malicious URL detected: \${{ TRIGGER.data.url }}"
|
|
271
|
+
priority: high
|
|
272
|
+
malice: malicious
|
|
273
|
+
status: new
|
|
274
|
+
payload:
|
|
275
|
+
url: \${{ TRIGGER.data.url }}
|
|
276
|
+
vt_malicious: \${{ ACTIONS.extract_verdict.result.malicious }}
|
|
277
|
+
vt_suspicious: \${{ ACTIONS.extract_verdict.result.suspicious }}
|
|
278
|
+
run_if: \${{ ACTIONS.extract_verdict.result.is_malicious }}
|
|
279
|
+
depends_on: [extract_verdict]`,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
id: "crowdsec_ip_blocker",
|
|
283
|
+
title: "CrowdSec IP Blocker",
|
|
284
|
+
description: "Add a malicious IP to CrowdSec blocklist with a timed ban decision. Source: Discord community contributors.",
|
|
285
|
+
category: "threat_response",
|
|
286
|
+
actions: 2,
|
|
287
|
+
yaml: `# CrowdSec IP Blocker
|
|
288
|
+
# Source: Tracecat Discord community contributors
|
|
289
|
+
# Trigger: webhook with { "ip_address": "x.x.x.x", "reason": "...", "duration": "24h" }
|
|
290
|
+
# Secrets required: crowdsec (keys: API_TOKEN)
|
|
291
|
+
|
|
292
|
+
definition:
|
|
293
|
+
title: CrowdSec IP Blocker
|
|
294
|
+
description: Add a malicious IP to CrowdSec blocklist with a timed ban
|
|
295
|
+
|
|
296
|
+
triggers:
|
|
297
|
+
- type: webhook
|
|
298
|
+
ref: block_request
|
|
299
|
+
|
|
300
|
+
actions:
|
|
301
|
+
- ref: add_decision
|
|
302
|
+
action: core.http_request
|
|
303
|
+
args:
|
|
304
|
+
url: "https://admin.api.crowdsec.net/v1/decisions"
|
|
305
|
+
method: POST
|
|
306
|
+
headers:
|
|
307
|
+
x-api-key: \${{ SECRETS.crowdsec.API_TOKEN }}
|
|
308
|
+
Content-Type: application/json
|
|
309
|
+
payload:
|
|
310
|
+
- scope: ip
|
|
311
|
+
value: \${{ TRIGGER.data.ip_address }}
|
|
312
|
+
type: ban
|
|
313
|
+
duration: \${{ TRIGGER.data.duration }}
|
|
314
|
+
origin: tracecat
|
|
315
|
+
scenario: "manual block via SOAR"
|
|
316
|
+
|
|
317
|
+
- ref: confirm_block
|
|
318
|
+
action: core.transform.reshape
|
|
319
|
+
args:
|
|
320
|
+
value:
|
|
321
|
+
status: blocked
|
|
322
|
+
ip: \${{ TRIGGER.data.ip_address }}
|
|
323
|
+
reason: \${{ TRIGGER.data.reason }}
|
|
324
|
+
duration: \${{ TRIGGER.data.duration }}
|
|
325
|
+
decision_id: \${{ ACTIONS.add_decision.result }}
|
|
326
|
+
depends_on: [add_decision]`,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "phishing_response",
|
|
330
|
+
title: "Phishing Email Response",
|
|
331
|
+
description: "Analyze a reported phishing email: extract IOCs, check URLs with VirusTotal, evaluate verdict, create case, and notify SOC. Source: Tracecat official website and documentation.",
|
|
332
|
+
category: "incident_response",
|
|
333
|
+
actions: 6,
|
|
334
|
+
yaml: `# Phishing Email Response
|
|
335
|
+
# Source: Tracecat official website (https://tracecat.com) and documentation
|
|
336
|
+
# Trigger: webhook with { "from": "...", "subject": "...", "body": "...", "urls": [...], "reporter": "..." }
|
|
337
|
+
# Secrets required: virustotal (keys: API_KEY), slack (keys: BOT_TOKEN)
|
|
338
|
+
|
|
339
|
+
definition:
|
|
340
|
+
title: Phishing Email Response
|
|
341
|
+
description: Analyze reported phishing emails, check URLs, verdict, create case and notify
|
|
342
|
+
|
|
343
|
+
triggers:
|
|
344
|
+
- type: webhook
|
|
345
|
+
ref: reported_email
|
|
346
|
+
|
|
347
|
+
actions:
|
|
348
|
+
- ref: parse_email
|
|
349
|
+
action: core.transform.reshape
|
|
350
|
+
args:
|
|
351
|
+
value:
|
|
352
|
+
sender: \${{ TRIGGER.data.from }}
|
|
353
|
+
subject: \${{ TRIGGER.data.subject }}
|
|
354
|
+
urls: \${{ TRIGGER.data.urls }}
|
|
355
|
+
reporter: \${{ TRIGGER.data.reporter }}
|
|
356
|
+
received_at: \${{ FN.now() }}
|
|
357
|
+
|
|
358
|
+
- ref: check_urls_vt
|
|
359
|
+
action: core.http_request
|
|
360
|
+
args:
|
|
361
|
+
url: "https://www.virustotal.com/api/v3/urls/\${{ FN.strip(FN.to_base64url(var.url)) }}"
|
|
362
|
+
method: GET
|
|
363
|
+
headers:
|
|
364
|
+
x-apikey: \${{ SECRETS.virustotal.API_KEY }}
|
|
365
|
+
Accept: application/json
|
|
366
|
+
control_flow:
|
|
367
|
+
for_each: \${{ ACTIONS.parse_email.result.urls }}
|
|
368
|
+
start_delay: 1
|
|
369
|
+
retry_policy:
|
|
370
|
+
max_attempts: 3
|
|
371
|
+
timeout: 30
|
|
372
|
+
depends_on: [parse_email]
|
|
373
|
+
|
|
374
|
+
- ref: evaluate_verdict
|
|
375
|
+
action: core.script.run_python
|
|
376
|
+
args:
|
|
377
|
+
script: |
|
|
378
|
+
import json
|
|
379
|
+
|
|
380
|
+
def main(vt_results_json, sender):
|
|
381
|
+
vt_results = json.loads(vt_results_json)
|
|
382
|
+
total_malicious = 0
|
|
383
|
+
malicious_urls = []
|
|
384
|
+
for r in vt_results:
|
|
385
|
+
if isinstance(r, dict) and "data" in r:
|
|
386
|
+
stats = r["data"].get("attributes", {}).get("last_analysis_stats", {})
|
|
387
|
+
mal = stats.get("malicious", 0)
|
|
388
|
+
total_malicious += mal
|
|
389
|
+
if mal > 0:
|
|
390
|
+
malicious_urls.append(r["data"]["id"])
|
|
391
|
+
is_phishing = total_malicious > 2
|
|
392
|
+
return {
|
|
393
|
+
"is_phishing": is_phishing,
|
|
394
|
+
"total_malicious_detections": total_malicious,
|
|
395
|
+
"malicious_urls": malicious_urls,
|
|
396
|
+
"confidence": "high" if total_malicious > 5 else "medium" if total_malicious > 0 else "low"
|
|
397
|
+
}
|
|
398
|
+
inputs:
|
|
399
|
+
vt_results_json: \${{ FN.serialize_json(ACTIONS.check_urls_vt.result) }}
|
|
400
|
+
sender: \${{ ACTIONS.parse_email.result.sender }}
|
|
401
|
+
depends_on: [check_urls_vt]
|
|
402
|
+
|
|
403
|
+
- ref: create_phishing_case
|
|
404
|
+
action: core.cases.create_case
|
|
405
|
+
args:
|
|
406
|
+
case_title: "[PHISH] \${{ ACTIONS.parse_email.result.subject }}"
|
|
407
|
+
priority: \${{ FN.conditional(ACTIONS.evaluate_verdict.result.is_phishing, "high", "medium") }}
|
|
408
|
+
malice: \${{ FN.conditional(ACTIONS.evaluate_verdict.result.is_phishing, "malicious", "unknown") }}
|
|
409
|
+
status: new
|
|
410
|
+
payload:
|
|
411
|
+
sender: \${{ ACTIONS.parse_email.result.sender }}
|
|
412
|
+
subject: \${{ ACTIONS.parse_email.result.subject }}
|
|
413
|
+
reporter: \${{ ACTIONS.parse_email.result.reporter }}
|
|
414
|
+
malicious_urls: \${{ ACTIONS.evaluate_verdict.result.malicious_urls }}
|
|
415
|
+
confidence: \${{ ACTIONS.evaluate_verdict.result.confidence }}
|
|
416
|
+
depends_on: [evaluate_verdict]
|
|
417
|
+
|
|
418
|
+
- ref: add_evidence_comment
|
|
419
|
+
action: core.cases.create_comment
|
|
420
|
+
args:
|
|
421
|
+
case_id: \${{ ACTIONS.create_phishing_case.result.id }}
|
|
422
|
+
content: "VT Analysis: \${{ ACTIONS.evaluate_verdict.result.total_malicious_detections }} malicious detections across \${{ FN.length(ACTIONS.parse_email.result.urls) }} URLs. Confidence: \${{ ACTIONS.evaluate_verdict.result.confidence }}. Malicious URLs: \${{ FN.serialize_json(ACTIONS.evaluate_verdict.result.malicious_urls) }}"
|
|
423
|
+
depends_on: [create_phishing_case]
|
|
424
|
+
|
|
425
|
+
- ref: notify_soc
|
|
426
|
+
action: core.http_request
|
|
427
|
+
args:
|
|
428
|
+
url: https://slack.com/api/chat.postMessage
|
|
429
|
+
method: POST
|
|
430
|
+
headers:
|
|
431
|
+
Authorization: "Bearer \${{ SECRETS.slack.BOT_TOKEN }}"
|
|
432
|
+
Content-Type: application/json
|
|
433
|
+
payload:
|
|
434
|
+
channel: "#soc-alerts"
|
|
435
|
+
blocks:
|
|
436
|
+
- type: header
|
|
437
|
+
text:
|
|
438
|
+
type: plain_text
|
|
439
|
+
text: "Phishing Report: \${{ ACTIONS.parse_email.result.subject }}"
|
|
440
|
+
- type: section
|
|
441
|
+
fields:
|
|
442
|
+
- type: mrkdwn
|
|
443
|
+
text: "*Sender:*\\n\${{ ACTIONS.parse_email.result.sender }}"
|
|
444
|
+
- type: mrkdwn
|
|
445
|
+
text: "*Verdict:*\\n\${{ FN.conditional(ACTIONS.evaluate_verdict.result.is_phishing, 'PHISHING', 'CLEAN') }}"
|
|
446
|
+
- type: mrkdwn
|
|
447
|
+
text: "*Confidence:*\\n\${{ ACTIONS.evaluate_verdict.result.confidence }}"
|
|
448
|
+
- type: mrkdwn
|
|
449
|
+
text: "*Reporter:*\\n\${{ ACTIONS.parse_email.result.reporter }}"
|
|
450
|
+
depends_on: [create_phishing_case]`,
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "siem_alert_triage",
|
|
454
|
+
title: "Generic SIEM Alert Triage",
|
|
455
|
+
description: "Receive SIEM alerts, check if source IP is public, enrich with VirusTotal, score severity, and create a case. Source: Tracecat Discord + community patterns.",
|
|
456
|
+
category: "alert_management",
|
|
457
|
+
actions: 4,
|
|
458
|
+
yaml: `# Generic SIEM Alert Triage
|
|
459
|
+
# Source: Tracecat Discord community + common SOAR patterns
|
|
460
|
+
# Trigger: webhook with { "alert_name": "...", "src_ip": "...", "severity": "...", "raw_log": "..." }
|
|
461
|
+
# Secrets required: virustotal (keys: API_KEY)
|
|
462
|
+
|
|
463
|
+
definition:
|
|
464
|
+
title: Generic SIEM Alert Triage
|
|
465
|
+
description: Triage SIEM alerts with IP enrichment and automated case creation
|
|
466
|
+
|
|
467
|
+
triggers:
|
|
468
|
+
- type: webhook
|
|
469
|
+
ref: siem_alert
|
|
470
|
+
|
|
471
|
+
actions:
|
|
472
|
+
- ref: check_ip_type
|
|
473
|
+
action: core.transform.reshape
|
|
474
|
+
args:
|
|
475
|
+
value:
|
|
476
|
+
alert_name: \${{ TRIGGER.data.alert_name }}
|
|
477
|
+
src_ip: \${{ TRIGGER.data.src_ip }}
|
|
478
|
+
severity: \${{ TRIGGER.data.severity }}
|
|
479
|
+
is_public: \${{ FN.ipv4_is_public(TRIGGER.data.src_ip) }}
|
|
480
|
+
|
|
481
|
+
- ref: enrich_with_vt
|
|
482
|
+
action: core.http_request
|
|
483
|
+
args:
|
|
484
|
+
url: "https://www.virustotal.com/api/v3/ip_addresses/\${{ ACTIONS.check_ip_type.result.src_ip }}"
|
|
485
|
+
method: GET
|
|
486
|
+
headers:
|
|
487
|
+
x-apikey: \${{ SECRETS.virustotal.API_KEY }}
|
|
488
|
+
Accept: application/json
|
|
489
|
+
run_if: \${{ ACTIONS.check_ip_type.result.is_public }}
|
|
490
|
+
depends_on: [check_ip_type]
|
|
491
|
+
|
|
492
|
+
- ref: score_alert
|
|
493
|
+
action: core.transform.reshape
|
|
494
|
+
args:
|
|
495
|
+
value:
|
|
496
|
+
alert_name: \${{ ACTIONS.check_ip_type.result.alert_name }}
|
|
497
|
+
src_ip: \${{ ACTIONS.check_ip_type.result.src_ip }}
|
|
498
|
+
is_public: \${{ ACTIONS.check_ip_type.result.is_public }}
|
|
499
|
+
vt_malicious: \${{ FN.conditional(ACTIONS.check_ip_type.result.is_public, ACTIONS.enrich_with_vt.result.data.attributes.last_analysis_stats.malicious, 0) }}
|
|
500
|
+
priority: \${{ FN.conditional(TRIGGER.data.severity == "critical", "critical", FN.conditional(TRIGGER.data.severity == "high", "high", FN.conditional(TRIGGER.data.severity == "medium", "medium", "low"))) }}
|
|
501
|
+
malice: \${{ FN.conditional(ACTIONS.check_ip_type.result.is_public && ACTIONS.enrich_with_vt.result.data.attributes.last_analysis_stats.malicious > 5, "malicious", "unknown") }}
|
|
502
|
+
depends_on: [enrich_with_vt]
|
|
503
|
+
|
|
504
|
+
- ref: create_triage_case
|
|
505
|
+
action: core.cases.create_case
|
|
506
|
+
args:
|
|
507
|
+
case_title: "[SIEM] \${{ ACTIONS.score_alert.result.alert_name }}"
|
|
508
|
+
priority: \${{ ACTIONS.score_alert.result.priority }}
|
|
509
|
+
malice: \${{ ACTIONS.score_alert.result.malice }}
|
|
510
|
+
status: new
|
|
511
|
+
payload:
|
|
512
|
+
src_ip: \${{ ACTIONS.score_alert.result.src_ip }}
|
|
513
|
+
is_public_ip: \${{ ACTIONS.score_alert.result.is_public }}
|
|
514
|
+
vt_malicious: \${{ ACTIONS.score_alert.result.vt_malicious }}
|
|
515
|
+
original_severity: \${{ TRIGGER.data.severity }}
|
|
516
|
+
raw_log: \${{ TRIGGER.data.raw_log }}
|
|
517
|
+
depends_on: [score_alert]`,
|
|
518
|
+
},
|
|
519
|
+
];
|
|
520
|
+
export function registerTemplateTools(server, _client) {
|
|
521
|
+
server.tool("tracecat_list_templates", "List available SOAR workflow templates. Returns template ID, title, description, category, and action count for each template.", {}, async () => {
|
|
522
|
+
if (TEMPLATES.length === 0) {
|
|
523
|
+
return {
|
|
524
|
+
content: [{ type: "text", text: "No templates available yet. Templates from official Tracecat documentation and community sources will be added here." }],
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
const list = TEMPLATES.map(({ id, title, description, category, actions }) => ({
|
|
528
|
+
id, title, description, category, actions,
|
|
529
|
+
}));
|
|
530
|
+
return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
|
|
531
|
+
});
|
|
532
|
+
server.tool("tracecat_get_template", "Get the full YAML template for a SOAR workflow pattern. Use tracecat_list_templates to see available template IDs.", {
|
|
533
|
+
template_id: z.string().describe("Template ID from tracecat_list_templates"),
|
|
534
|
+
}, async ({ template_id }) => {
|
|
535
|
+
const template = TEMPLATES.find((t) => t.id === template_id);
|
|
536
|
+
if (!template) {
|
|
537
|
+
const available = TEMPLATES.map((t) => t.id).join(", ") || "(none)";
|
|
538
|
+
return {
|
|
539
|
+
content: [{ type: "text", text: `Unknown template: ${template_id}. Available templates: ${available}` }],
|
|
540
|
+
isError: true,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text", text: `# Template: ${template.title}\n\n${template.description}\n\n${template.yaml}` }],
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerWebhookTools(server, client) {
|
|
3
|
+
server.tool("tracecat_create_webhook_key", "Generate or rotate a webhook API key for a workflow", {
|
|
4
|
+
workflow_id: z.string().describe("Workflow ID"),
|
|
5
|
+
}, async ({ workflow_id }) => {
|
|
6
|
+
const result = await client.post(`/workflows/${workflow_id}/webhook/api-key`);
|
|
7
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
8
|
+
});
|
|
9
|
+
}
|