From a072845a3f0ccee58ca474dcc56d1bb6ba9079a7 Mon Sep 17 00:00:00 2001 From: MAGI Date: Sun, 22 Mar 2026 16:06:14 -0600 Subject: [PATCH] Fix review comments: correct AWS Detective API usage and forensic ordering - Fix FilterCriteria to use singular Severity/Status with Value objects instead of invalid plural Severities/Statuses arrays (SKILL.md + process.py) - Fix get_entity_history: rename to get_investigation_indicators, use investigation_id instead of entity_arn for InvestigationId parameter - Replace invalid inv-* placeholders with 21-digit numeric IDs - Fix Expected Output to match real API response structure (no embedded Indicators; document separate list-indicators call and indicator types) - Fix CLI --filter-criteria example to use correct format - Update process.py --severity to accept single value with validation - Add --max-results validation (1-100 range) - Add pagination via _collect_all_pages helper for all list API calls - Reorder Response Actions checklist: evidence preservation before containment - Reorder Phase 5 workflow: preserve evidence first when safe --- .../SKILL.md | 40 ++++++++--------- .../assets/template.md | 2 +- .../references/workflows.md | 9 ++-- .../scripts/process.py | 43 +++++++++++++------ 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/skills/performing-cloud-native-threat-hunting-with-aws-detective/SKILL.md b/skills/performing-cloud-native-threat-hunting-with-aws-detective/SKILL.md index 83bcd2cd..0b0ae103 100644 --- a/skills/performing-cloud-native-threat-hunting-with-aws-detective/SKILL.md +++ b/skills/performing-cloud-native-threat-hunting-with-aws-detective/SKILL.md @@ -46,7 +46,7 @@ aws detective list-graphs --output table # Get entity profile for an IAM user aws detective get-investigation \ --graph-arn arn:aws:detective:us-east-1:123456789012:graph:a1b2c3d4 \ - --investigation-id inv-1234567890 + --investigation-id 000000000000000000001 ``` ### Step 3: Search Entities Programmatically @@ -65,29 +65,26 @@ def list_behavior_graphs(): response = detective.list_graphs() return response.get('GraphList', []) -def get_entity_history(graph_arn, entity_arn, hours=24): - """Get API call history for an entity.""" - end_time = datetime.utcnow() - start_time = end_time - timedelta(hours=hours) - +def get_investigation_indicators(graph_arn, investigation_id, max_results=50): + """Get indicators for a specific investigation.""" response = detective.list_indicators( GraphArn=graph_arn, - InvestigationId=entity_arn, - MaxResults=50 + InvestigationId=investigation_id, + MaxResults=max_results ) return response.get('Indicators', []) def investigate_guardduty_findings(graph_arn): - """List finding groups correlated by Detective.""" + """List high-severity investigations correlated by Detective.""" response = detective.list_investigations( GraphArn=graph_arn, FilterCriteria={ - 'Statuses': ['RUNNING', 'FAILED'], - 'Severities': ['HIGH', 'CRITICAL'] + 'Severity': {'Value': 'CRITICAL'}, + 'Status': {'Value': 'RUNNING'} }, MaxResults=20 ) - + for investigation in response.get('InvestigationDetails', []): print(f"Investigation: {investigation['InvestigationId']}") print(f" Entity: {investigation['EntityArn']}") @@ -109,7 +106,7 @@ if __name__ == "__main__": # List investigations with high severity aws detective list-investigations \ --graph-arn arn:aws:detective:us-east-1:123456789012:graph:a1b2c3d4 \ - --filter-criteria '{"Severities":["HIGH","CRITICAL"]}' \ + --filter-criteria '{"Severity":{"Value":"HIGH"}}' \ --max-results 10 ``` @@ -119,33 +116,32 @@ aws detective list-investigations \ # Get indicators for a specific investigation aws detective list-indicators \ --graph-arn arn:aws:detective:us-east-1:123456789012:graph:a1b2c3d4 \ - --investigation-id inv-1234567890 \ + --investigation-id 000000000000000000001 \ --max-results 50 ``` ## Expected Output +The `list-investigations` command returns investigation metadata: + ```json { "InvestigationDetails": [ { - "InvestigationId": "inv-0a1b2c3d4e5f", + "InvestigationId": "000000000000000000001", "Severity": "CRITICAL", "Status": "RUNNING", + "State": "ACTIVE", "EntityArn": "arn:aws:iam::123456789012:user/suspicious-user", "EntityType": "IAM_USER", - "CreatedTime": "2026-03-15T14:30:00Z", - "Indicators": { - "ImpossibleTravel": true, - "NewGeoLocation": "ru-central-1", - "UnusualApiCalls": ["CreateAccessKey", "AttachUserPolicy", "PutBucketPolicy"], - "RelatedFindings": 7 - } + "CreatedTime": "2026-03-15T14:30:00Z" } ] } ``` +Indicators are retrieved separately via `list-indicators` and include types such as `TTP_OBSERVED`, `IMPOSSIBLE_TRAVEL`, `FLAGGED_IP_ADDRESS`, `NEW_GEOLOCATION`, `NEW_ASO`, `NEW_USER_AGENT`, `RELATED_FINDING`, and `RELATED_FINDING_GROUP`. + ## Verification 1. Confirm behavior graph has data: `aws detective list-graphs` returns non-empty list diff --git a/skills/performing-cloud-native-threat-hunting-with-aws-detective/assets/template.md b/skills/performing-cloud-native-threat-hunting-with-aws-detective/assets/template.md index 5924e83c..66eb65e5 100644 --- a/skills/performing-cloud-native-threat-hunting-with-aws-detective/assets/template.md +++ b/skills/performing-cloud-native-threat-hunting-with-aws-detective/assets/template.md @@ -27,8 +27,8 @@ - [ ] Initial access vector identified ## Response Actions +- [ ] Evidence preserved (or capture rationale if immediate containment required) - [ ] Compromised credentials disabled - [ ] Active sessions revoked - [ ] Affected resources isolated -- [ ] Evidence preserved - [ ] Stakeholders notified diff --git a/skills/performing-cloud-native-threat-hunting-with-aws-detective/references/workflows.md b/skills/performing-cloud-native-threat-hunting-with-aws-detective/references/workflows.md index 688b70f1..69f32f74 100644 --- a/skills/performing-cloud-native-threat-hunting-with-aws-detective/references/workflows.md +++ b/skills/performing-cloud-native-threat-hunting-with-aws-detective/references/workflows.md @@ -24,7 +24,8 @@ 4. Document indicators of compromise (IOCs) ## Phase 5: Response -1. Disable compromised credentials -2. Revoke active sessions -3. Isolate affected resources -4. Preserve evidence (CloudTrail logs, flow logs) +1. Preserve evidence (CloudTrail logs, flow logs, snapshots) when safe +2. Disable compromised credentials +3. Revoke active sessions +4. Isolate affected resources +5. If active impact is ongoing, contain first and document evidence trade-offs diff --git a/skills/performing-cloud-native-threat-hunting-with-aws-detective/scripts/process.py b/skills/performing-cloud-native-threat-hunting-with-aws-detective/scripts/process.py index d9bc1658..6cf15f4f 100644 --- a/skills/performing-cloud-native-threat-hunting-with-aws-detective/scripts/process.py +++ b/skills/performing-cloud-native-threat-hunting-with-aws-detective/scripts/process.py @@ -13,11 +13,23 @@ import os from datetime import datetime, timedelta +def _collect_all_pages(client_method, result_key, **kwargs): + """Paginate through all pages of an AWS Detective API call.""" + all_items = [] + while True: + response = client_method(**kwargs) + all_items.extend(response.get(result_key, [])) + next_token = response.get('NextToken') + if not next_token: + break + kwargs['NextToken'] = next_token + return all_items + + def list_behavior_graphs(session): """List all Detective behavior graphs in the account.""" client = session.client('detective') - response = client.list_graphs() - graphs = response.get('GraphList', []) + graphs = _collect_all_pages(client.list_graphs, 'GraphList') if not graphs: print("[!] No behavior graphs found. Enable Detective first.") @@ -33,13 +45,13 @@ def list_behavior_graphs(session): return graphs -def list_investigations(session, graph_arn, severities=None, max_results=20): +def list_investigations(session, graph_arn, severity=None, max_results=20): """List investigations filtered by severity.""" client = session.client('detective') filter_criteria = {} - if severities: - filter_criteria['Severities'] = severities + if severity: + filter_criteria['Severity'] = {'Value': severity} kwargs = { 'GraphArn': graph_arn, @@ -48,8 +60,9 @@ def list_investigations(session, graph_arn, severities=None, max_results=20): if filter_criteria: kwargs['FilterCriteria'] = filter_criteria - response = client.list_investigations(**kwargs) - investigations = response.get('InvestigationDetails', []) + investigations = _collect_all_pages( + client.list_investigations, 'InvestigationDetails', **kwargs + ) if not investigations: print("[+] No investigations found matching criteria") @@ -96,13 +109,12 @@ def list_indicators(session, graph_arn, investigation_id, max_results=50): """List indicators for a specific investigation.""" client = session.client('detective') - response = client.list_indicators( + indicators = _collect_all_pages( + client.list_indicators, 'Indicators', GraphArn=graph_arn, InvestigationId=investigation_id, MaxResults=max_results, ) - - indicators = response.get('Indicators', []) if not indicators: print("[+] No indicators found for this investigation") return [] @@ -150,11 +162,13 @@ if __name__ == "__main__": ) parser.add_argument( "--severity", - nargs="+", + type=str, default=None, - help="Severity filter (e.g. HIGH CRITICAL)", + choices=["INFORMATIONAL", "LOW", "MEDIUM", "HIGH", "CRITICAL"], + help="Severity filter (e.g. HIGH)", ) - parser.add_argument("--max-results", type=int, default=20) + parser.add_argument("--max-results", type=int, default=20, + help="Max results per API call (1-100)") parser.add_argument("--region", default="us-east-1") parser.add_argument("--profile", type=str, help="AWS profile name") parser.add_argument( @@ -162,6 +176,9 @@ if __name__ == "__main__": ) args = parser.parse_args() + if args.max_results < 1 or args.max_results > 100: + parser.error("--max-results must be between 1 and 100") + kwargs = {"region_name": args.region} if args.profile: kwargs["profile_name"] = args.profile