Agent Skills › TracecatHQ/tracecat › guardduty-case-lifecycle

guardduty-case-lifecycle

GitHub

自动化处理AWS GuardDuty安全告警,从获取数据、去重、关联案件到生成Slack通知和证据表的全流程编排。

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

Trigger Scenarios

收到GuardDuty告警ID或手动运行请求 需要对新发现进行初步分类和响应

Install

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

Non-standard path

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

Use without installing

npx skills use TracecatHQ/tracecat@guardduty-case-lifecycle

指定 Agent (Claude Code)

npx skills add TracecatHQ/tracecat --skill guardduty-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": "guardduty-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 hands you a GuardDuty finding to triage, either a finding id with its region and detector or a manual run carrying finding_id or since. Takes the finding from AWS to a triaged Tracecat case and a published Slack alert. You fetch it with boto3, map its family, dedupe it, attach it to a case or create one, and stop when a person has already decided. Otherwise you investigate, 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."
}

GuardDuty case lifecycle

You own a GuardDuty finding from the moment it reaches you until the team can act on it in Slack. A finding reaches you one per run: relayed through a detection event, from a manual run, or from a person in chat. No step before you decides anything about it. Every decision 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 Finding <id> unchanged 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
Enabled regions discover with ec2 describe_regions in us-east-1 when you need them
GuardDuty console link https://<region>.console.aws.amazon.com/guardduty/home?region=<region>#/findings?fId=<finding_id>
Time format in anything a reader sees 17 Sep 2026, 03:47 UTC

AWS calls 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. Fetch the finding

  • Finding id with region and detector. The prompt gives finding_id, region and detector_id. Call guardduty get_findings in that region with {"DetectorId": <detector_id>, "FindingIds": [<finding_id>]}.
  • Manual run with a finding ARN. The ARN reads arn:aws:guardduty:<region>:<account>:detector/<detector_id>/finding/<finding_id>. Take all three values from it and call get_findings as above.
  • Manual run with a bare finding id. List the enabled regions. In each region, call list_detectors then get_findings until one returns the finding.
  • Manual run with since. In each enabled region, call list_findings with FindingCriteria updatedAt GreaterThanOrEqual set to since in epoch milliseconds, SortCriteria {"AttributeName": "updatedAt", "OrderBy": "DESC"} and MaxResults 1. Take the newest across regions and fetch it.
  • Not found anywhere. Return Finding <id> not found in any enabled region. and stop.

Read these from the finding and keep them for every later stage:

Value Where it comes from
id, account, region, type, severity id, accountId, region, type, severity
count service.count
first seen, last seen service.eventFirstSeen, service.eventLastSeen
principal resource.accessKeyDetails.principalId, else resource.kubernetesDetails.kubernetesUserDetails.username
resource the first present of resource.instanceDetails.instanceId, resource.kubernetesDetails.kubernetesWorkloadDetails.name, resource.eksClusterDetails.name, resource.s3BucketDetails[0].name, resource.accessKeyDetails.userName, resource.accessKeyDetails.accessKeyId
sample service.additionalInfo.value parsed as JSON, key sample. Only true marks a GuardDuty sample
indicator service.action.dnsRequestAction.domain, else the remoteIpDetails.ipAddressV4 under networkConnectionAction, awsApiCallAction or kubernetesApiCallAction

2. Map the family

Map the finding type to one family with the ordered rules in families.md. The first matching rule wins. The family feeds the group tag and the hypothesis library.

3. Find the case

  1. Same finding. Call core.cases.search_cases with tags ["finding-<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. Nothing new (automated runs only). On a hit, read the case's last_seen field. When it equals the finding's last seen time as an instant, the finding has not recurred. Return Finding <id> unchanged on <short_id>; nothing to do. and stop. A manual run always continues, because a person asked for a re-triage.
  3. Same group. With no finding hit, search tags ["group-<family>-<account>"] 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 id appended if absent.
    • first_seen: keep the existing value; set it only when empty.
    • finding_count, finding_type, last_seen, principal, region, resource: from this finding.
    • payload.finding_summary: {account, count, finding_ids, first_seen, last_seen, region, resource_id, severity, type}.
  5. Create when neither search hit. Call core.cases.create_case with:
    • summary Triage in progress: <type>
    • description Triage in progress.
    • status new, severity low
    • create_missing_tags true
    • tags guardduty, family-<family>, account-<account>, group-<family>-<account>, finding-<id>
    • fields aws_account, finding_count, finding_ids ([<id>]), finding_type, first_seen, last_seen, principal, region, resource
    • 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:

Finding <id> (<type>) recurred 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.

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. Build the investigation record that aws-cloud-incident-response-core defines. Gather these facts yourself:

  • Every finding on the case, in full. Call get_findings for each id in finding_ids, in the case's region first. Look for any id it does not return in the other enabled regions, or in the SIEM's GuardDuty records. Triage the case, not only this finding.

  • 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 finding'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.
    • A finding with no domain and no address has no external indicator. Say so rather than leaving the field blank.
  • Shared workload role sessions for any finding on shared clusters. 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. 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.

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. Run case-output's self-check 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 finding's principal, else empty.

Add each tag with core.cases.add_case_tag and create_if_missing true: guardduty, family-<family>, account-<account>, group-<family>-<account> and finding-<id>. 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:

Finding <id> (<type>, <first seen, reader format>) attached and the case re-triaged. Status: <disposition status> (<confidence> confidence). <reason> Case findings: <number of finding 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/detection-event-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
fb60aee7
Indexed
2026-10-20 03:53

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