Agent Skills › TracecatHQ/tracecat › detection-event-case-lifecycle

detection-event-case-lifecycle

GitHub

自动化处理SIEM检测事件,完成去重、案件创建、证据调查及Slack通知。适用于安全运营中心告警响应与案件生命周期管理。

examples/001-distributed-alerting/skills/detection-event-case-lifecycle/SKILL.md TracecatHQ/tracecat

Trigger Scenarios

提示以 Detection event <alert_id> 开头 包含JSON格式的SIEM告警数据

Install

npx skills add TracecatHQ/tracecat --skill detection-event-case-lifecycle -g -y
More Options

Non-standard path

npx skills add https://github.com/TracecatHQ/tracecat/tree/main/examples/001-distributed-alerting/skills/detection-event-case-lifecycle -g -y

Use without installing

npx skills use TracecatHQ/tracecat@detection-event-case-lifecycle

指定 Agent (Claude Code)

npx skills add TracecatHQ/tracecat --skill detection-event-case-lifecycle -a claude-code -g -y

安装 repo 全部 skill

npx skills add TracecatHQ/tracecat --all -g -y

预览 repo 内 skill

npx skills add TracecatHQ/tracecat --list

SKILL.md

Frontmatter
{
    "name": "detection-event-case-lifecycle",
    "metadata": {
        "tools": [
            "tools.aws_boto3.call_api",
            "core.cases.search_cases",
            "core.cases.get_case",
            "core.cases.create_case",
            "core.cases.update_case",
            "core.cases.add_case_tag",
            "core.cases.create_comment",
            "core.table.lookup",
            "core.table.insert_row",
            "tools.slack_sdk.call_method",
            "tools.slack.post_message",
            "tools.slack.update_message",
            "tools.slack.list_replies",
            "tools.slack.lookup_user_by_email"
        ]
    },
    "description": "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."
}

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

  1. Same alert. Call core.cases.search_cases with tags ["alert-<id>"], status ["new", "in_progress", "on_hold"], start_time 90 days ago, order_by created_at, sort desc, limit 1. It is a hit when the returned case carries that tag.
  2. Already published. An alert does not change after it is raised. On a same-alert hit, call core.table.lookup on slack_work_objects with column external_ref_id and the case id. With a row, the alert is triaged and published. Return Alert <id> already published on <short_id>; nothing to do. and stop. With no row, an earlier run did not finish: continue on that case.
  3. Same group. With no alert hit, search tags ["group-<group key>"] with the same statuses, start_time 24 hours ago, newest first, limit 1. It is a hit when the case carries that tag and carries neither owner-confirmed nor owner-denied.
  4. Attach on either hit. Call core.cases.update_case on 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 is not recorded.
    • payload.finding_summary: {source: "<siem>", detection_id, account, count, finding_ids, first_seen, last_seen, region, resource_id, severity, type}, where type is the detection name and severity the alert severity.
  5. Create when neither search hit. Call core.cases.create_case with:
    • summary Triage in progress: <detection title>
    • description Triage in progress.
    • status new, severity low
    • create_missing_tags true
    • tags detection-event, source-<siem>, detection-<detection name>, group-<group key>, alert-<id>, and account-<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 is not 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-confirmed or owner-denied;
  • its disposition field is Inconclusive.

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 and payload.finding_summary are 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 eventSource and eventName mean 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_cases with 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 AssumeRole sessions 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>:

    SELECT substring(JSONExtractString(rawLog, 'requestParameters', 'roleSessionName'), 1, <tenant prefix length>) AS tenant_session,
           JSONExtractString(rawLog, 'requestParameters', 'roleArn') AS assumed_role,
           count() AS events, min(receivedAt) AS first_event, max(receivedAt) AS last_event,
           arrayStringConcat(arrayMap(x -> toString(x), arraySort(groupUniqArray(20)(toMinute(receivedAt)))), ',') AS minutes_of_hour
    FROM aws_cloudtrail_logs
    WHERE receivedAt >= parseDateTimeBestEffort('<first seen>') - INTERVAL 2 MINUTE
      AND receivedAt <= parseDateTimeBestEffort('<last seen>') + INTERVAL 2 MINUTE
      AND eventName = 'AssumeRole'
      AND JSONExtractString(rawLog, 'userIdentity', 'arn') LIKE '%<your-shared-workload-role>%'
    GROUP BY tenant_session, assumed_role ORDER BY events DESC LIMIT 10
    

    An empty result means no tenant workload assumed a role in the window. Say that. When business-context names no shared workload role, skip this check.

6. Decide the values the case carries

Apply these rules exactly. They come out the same way every time for the same investigation. The case severity comes from your disposition, never from the alert's own severity; the alert severity is a fact you report.

Value Rule
Status Your disposition status, except that Benign with a linked email reads Open until the linked person answers
Severity Escalate → critical; Benign with no linked email → low; everything else → medium
Reason Your disposition reason with any trailing space or full stop removed
Due date field now plus 3 days, ISO 8601 (the only place ISO is used)

7. Write the case

Load case-output. Write the report and assemble the description exactly as case-output's "Case description" section lays it out, with the status line, the nine headings, the closure sentences and the Appendix Report line. case-output is written for GuardDuty findings, so read it with these substitutions, and nothing else changed:

case-output says For a detection event
values from guardduty-case-lifecycle stage 6 values from stage 6 of this skill
the finding, the finding type, GuardDuty's own description the alert, the detection name in inline code, and what the rule detects (the rule's description when the alert carries one, else the name and categories in plain words, nothing added)
the findings table, one row per finding, each id linked to the GuardDuty console the same table with the same six columns, one row per alert on the case, each alert id linked to its alert link from "Fixed values"; the Type column holds the detection name
every finding ID links to the GuardDuty console every alert ID links to its alert link
Appendix Finding IDs: Alert IDs:
title <Finding family in plain words> on <resource or linked person> in <deployment name> <what the rule detected, in plain words> on <resource or linked person> in <deployment name>, 60 characters or fewer
card rule row: <service>: <what was detected> the same shape, from the detection, for example IAM: policy attached to a role
self-check: at least one GuardDuty console link at least one alert link, and no GuardDuty console link unless a relayed GuardDuty finding was read

Run case-output's self-check with those substitutions and fix anything it catches before you write.

Then call core.cases.update_case on the case with:

  • summary: your case title.
  • description: the assembled description.
  • severity: from stage 6.
  • fields:
    • disposition: the status from stage 6.
    • due_date: from stage 6.
    • linked_person: the linked person, or empty.
    • principal: the actor principal, else the alert's principal, else empty.

Add each tag with core.cases.add_case_tag and create_if_missing true: detection-event, source-<siem>, detection-<detection name>, group-<group key>, alert-<id>, and account-<account> when there is one. Add escalation too when the status is Escalate. Leave the case's other tags as they are, and never remove escalation.

When you attached rather than created, add one comment:

Alert <id> (<detection name>, <first seen, reader format>) attached and the case re-triaged. Status: <disposition status> (<confidence> confidence). <reason> Case alerts: <number of alert ids>.

8. Publish to Slack

Follow slack-delivery.md in this skill. It covers the card, the brief, the evidence table, the owner ask and the slack_work_objects row, both for a new case and for re-triage of one already posted.

Boundaries

  • Read-only toward AWS, always.
  • Never close a case, never set it to resolved, and never remove escalation.
  • A case you create or update carries only what your evidence supports. Containment is a decision for a person and never an action of yours.

Version History

  • 2311ad8 Current 2026-10-20 03:53

Same Skill Collection

.agents/skills/gh-prerelease/SKILL.md
.agents/skills/gh-release/SKILL.md
.agents/skills/make-pr/SKILL.md
.agents/skills/test-tracecat-prompts/SKILL.md
.agents/skills/tracecat-automation-best-practices/SKILL.md
.agents/skills/tracecat-qa/SKILL.md
.agents/skills/tracecat-slackbot-best-practices/SKILL.md
examples/002-google-oauth-app-lifecycle/skills/case-writing-style/SKILL.md
examples/002-google-oauth-app-lifecycle/skills/org-chart/SKILL.md
examples/002-google-oauth-app-lifecycle/skills/third-party-vendor-research/SKILL.md
.agents/skills/docs-authoring/SKILL.md
examples/001-distributed-alerting/skills/aws-cloud-incident-response-core/SKILL.md
examples/001-distributed-alerting/skills/case-output/SKILL.md
examples/001-distributed-alerting/skills/guardduty-case-lifecycle/SKILL.md
examples/001-distributed-alerting/skills/hypothesis-driven-triage/SKILL.md
examples/001-distributed-alerting/skills/slack-case-threads/SKILL.md
examples/002-google-oauth-app-lifecycle/skills/google-oauth-app-lifecycle/SKILL.md
examples/002-google-oauth-app-lifecycle/skills/oauth-grant-review/SKILL.md

Metadata

Files
0
Version
2311ad8
Hash
9753cc3e
Indexed
2026-10-20 03:53

홈 - 위키
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-10-21 01:37
浙ICP备14020137号-1