tracecathq/tracecat/detection-event-case-lifecycle
- community
Checking that the SKILL.md file is still in the repository.
Load when the prompt starts with "Detection event <alert_id>" followed by the alert as JSON. Takes one detection event from the SIEM (in either the API shape or the webhook shape) to a triaged Tracecat case and a published Slack alert. You read the alert from the prompt, dedupe it, attach it to a case or create one, and stop when the alert is already published or a person has already decided. Otherwise you investigate the underlying events in the SIEM, write the case, post or refresh the Work Object card, the brief, the evidence table and the owner ask, and keep the slack_work_objects row current. Load with business-context (when that skill exists), hypothesis-driven-triage, aws-cloud-incident-response-core and case-output.
Install
npx vskill@latest install tracecathq/tracecat/detection-event-case-lifecycleSKILL.md
Detection event case lifecycle
You own a detection event from the moment it reaches you until the team can act on it in Slack. A detection event is one alert raised by a detection rule in the SIEM. The only deterministic steps before you are the receiver, which stores each alert in the detection_events table, and Triage alerts, which reads the row and starts one run of you per alert. Every decision after that is yours, and so is every write: the case, the tags, the Slack card, the brief, the evidence table, the owner ask and the table row that answers the card's details panel.
Work through the stages below in order. Each stage says when to stop. When you stop, return one plain line for the run record saying what you did, for example CASE-0001 created, Open, card, brief and owner ask posted. or Alert <id> already published on CASE-0001; nothing to do.
Fixed values
| Name | Value | |
|---|---|---|
| Workspace case URL | https://<your-tracecat-host>/workspaces/<workspace-id>/cases/<case_id> | |
| Triage channel | #security-alerts, C0123456789 | |
| Work Object table | slack_work_objects | |
| Alert link | the alert's resultLink when it has one, else <siem-alert-url> built from the alert id | |
| Time format in anything a reader sees | 17 Sep 2026, 03:47 UTC |
AWS calls, when you need one, use tools.aws_boto3.call_api with service_name, method_name, params and region_name. The configured credential already lands in your organisation's security audit account through <your-read-only-audit-role>, so leave role_arn empty. Call only read methods (get_*, list_*, describe_*).
1. Read the alert
The prompt is Detection event <alert_id> on the first line, then the alert as one JSON object. Nothing needs fetching: the JSON is the alert. It arrives in one of two shapes. Tell them apart by the top-level detection key. The field names below are one SIEM's alert format: <adapt to your SIEM>.
| Value | API shape (no detection key) | Webhook shape (alert under detection) | |
|---|---|---|---|
| alert id | id | detection.id | |
| detection name | detectionName | detection.name | |
| detection title | the detection name | detection.displayName | |
| what the rule detects | not carried; use the name and categories | detection.description | |
| rule id | detectionID | detection.detectionID | |
| alert severity | severity | detection.severity, with detection.riskScore | |
| raised at | createdAt | detection.executedAt | |
| categories, techniques | categories, mitreAttacks | detection.categories, detection.mitreAttacks, detection.mitreTechniques | |
| rows | results is a JSON string holding one row as an array. Parse it and pair it with columnNames by position to get a row object. recordsReturned is the row count | detection.results is a list of row objects; detection.resultCount is the count | |
| query window | not carried | detection.params.from and detection.params.to | |
| actor, source address | actor (id, email, username), srcIP | from the rows | |
| link | build it from the alert id | detection.resultLink | |
| run id | scheduledRunID | not carried |
A column name can repeat in columnNames (for example eventName twice). Keep the first value for each name.
The field mapping in this stage is one example. For an alert from another source (a cloud threat finding, an identity alert, an endpoint alert), map that payload to the same case fields. Leave a field empty when the payload does not have it, and say so in the case.
Read these from the alert and its row and keep them for every later stage. Column names differ by rule, so take the first that is present, and write not recorded when none is:
| Value | Where it comes from | |
|---|---|---|
| id, detection name, alert severity, raised at | the table above | |
| account | row recipientAccountId, else userIdentity.accountId, else an accountId or account column | |
| region | row awsRegion, else region, else the first entry of a regions column | |
| principal | row userIdentity.arn, else actor.id, else userIdentity.principalId, else a username, user or email column | |
| resource | the first present of the row's request target columns (requestParameters.roleName, requestParameters.bucketName, requestParameters.groupId, requestParameters.instanceId, requestParameters.policyArn), else a resource column, else not recorded | |
| first seen, last seen | row first_event and last_event, else eventTime for both, else the query window, else raised at for both | |
| count | the row's own count column when it has one (creation_count, count, events), else the row count | |
| indicator | srcIP, else row sourceIPAddress, when it is a public address and not an AWS service name; a domain column when the rule has one |
The row is the starting point, not the evidence. The events behind it are in the SIEM. Stage 5 reads them through the SIEM MCP over the alert's own window.
A GuardDuty finding relayed through the SIEM. When the detection is a GuardDuty finding passed along (the name or categories say guardduty, or the row carries a finding id or a finding ARN), take the finding id, region and detector id from the row and read the finding in full with guardduty get_findings, as guardduty-case-lifecycle stage 1 describes. Use it as evidence in stage 5. The case identity, tags and Slack messages stay as this skill defines them.
2. Name the group
The detection name is the family. It feeds the group tag, and in hypothesis-driven-triage's hypothesis-library.md you choose the family whose questions are closest to what the rule detects (a policy or permission change reads as config-change, use of credentials as credential-use, a command in a cluster as k8s-operator); when none fits, use generic.
The group key is <detection name>-<account>. With no account, use the principal's last path segment in lower case, with every character outside a-z, 0-9 and - replaced by -, cut to 40 characters. With neither, the group key is the detection name alone.
3. Find the case
- Same alert. Call
core.cases.search_caseswith tags["alert-<id>"], status["new", "in_progress", "on_hold"],start_time90 days ago,order_bycreated_at,sortdesc,limit1. It is a hit when the returned case carries that tag. - Already published. An alert does not change after it is raised. On a same-alert hit, call
core.table.lookuponslack_work_objectswith columnexternal_ref_idand the case id. With a row, the alert is triaged and published. ReturnAlert <id> already published on <short_id>; nothing to do.and stop. With no row, an earlier run did not finish: continue on that case. - Same group. With no alert hit, search tags
["group-<group key>"]with the same statuses,start_time24 hours ago, newest first,limit1. It is a hit when the case carries that tag and carries neitherowner-confirmednorowner-denied. - Attach on either hit. Call
core.cases.update_caseon that case. Set these fields:
finding_ids: the existing list, with this alert id appended if absent. The field is labelled Finding IDs; for a detection event it holds alert ids.first_seen: keep the existing value; set it only when empty.finding_count,finding_type(the detection name),last_seen,principal,region,resource: from this alert. Leave out any that isnot recorded.payload.finding_summary:{source: "<siem>", detection_id, account, count, finding_ids, first_seen, last_seen, region, resource_id, severity, type}, wheretypeis the detection name andseveritythe alert severity.
- Create when neither search hit. Call
core.cases.create_casewith:
- summary
Triage in progress: <detection title> - description
Triage in progress. - status
new, severitylow create_missing_tagstrue- tags
detection-event,source-<siem>,detection-<detection name>,group-<group key>,alert-<id>, andaccount-<account>when the row gives an account - fields
aws_account(only when the account is an AWS account id),finding_count,finding_ids([<id>]),finding_type,first_seen,last_seen,principal,region,resource, leaving out any that isnot recorded - the same
payload.finding_summary
Keep the case id, its short id (CASE-0001), its URL, and whether you created it or attached to it.
4. Stop if a person has already decided
An attached case counts as decided when either is true:
- it carries
owner-confirmedorowner-denied; - its
dispositionfield isInconclusive.
On a decided case, add one comment with core.cases.create_comment and stop:
Alert <id> (<detection name>) was raised again with <count> events, last seen <last seen, reader format>, and was attached. A person has already decided this case (<the decided tags and disposition, comma separated>), so the description, disposition and severity were left unchanged and no new triage ran.
Write 1 event when the count is 1.
5. Investigate
Load business-context if you have that skill, then hypothesis-driven-triage and aws-cloud-incident-response-core, and follow the method there. Those skills are written around GuardDuty findings. Read "finding" as "alert", "finding type" as "detection name", and where they say to fetch the finding with boto3, read the alert you were given and the events behind it in the SIEM instead. Build the investigation record that aws-cloud-incident-response-core defines. Gather these facts yourself:
- Every alert on the case. This alert is in the prompt. For each other id in
finding_ids, the facts recorded in the case description andpayload.finding_summaryare what you have; say so where they limit a claim. Triage the case, not only this alert. - The events behind the alert. With the SIEM MCP, read the source records the rule matched, for the alert's principal and resource, from 15 minutes before first seen to 15 minutes after last seen. Establish what was done, whether it succeeded, from where, and with which session. The row tells you which records: an
eventSourceandeventNamemean the account activity records. Then widen to what the same principal did in the hour around it. - Report times.
- Report time is now.
- The due time is now plus 3 days; the next update is now plus 1 day.
- The activity is still happening when the last event is within 24 hours of now.
- The alert's own indicator. Look it up with the threat enrichment tools you have (domain first, else the address), and with an IP reputation tool for an address.
- A first-seen date within a few minutes of now means the enrichment service had never seen the indicator before this lookup. Zero detections then means no prior record, not a clean verdict.
- An alert with no domain and no public address has no external indicator. Say so rather than leaving the field blank.
- History. Prior cases for the same rule (
core.cases.search_caseswith tags["detection-<detection name>"], 90 days) with their outcomes, and how often the same principal did the same thing over 90 days in the SIEM. A rule that fires on the same automated principal every day is a pattern to name, with its count. - Shared workload role sessions, only when the actor is a shared workload role. Some organisations run many tenants or jobs through one shared role that assumes other roles on their behalf. business-context names that role and its session naming pattern when your organisation has one. When the principal is that role or a session it assumed, query the SIEM for the role's cross-account
AssumeRolesessions from 2 minutes before first seen to 2 minutes after last seen, grouped by tenant session prefix and role. The query below shows the logic in one SQL dialect; table, column and function names are<adapt to your SIEM>:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingexamples/001-distributed-alerting/skills/detection-event-case-lifecycle/SKILL.md
Popularity
- 7-day trend score
- 0
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/tracecathq/tracecat/detection-event-case-lifecycle)