Do not reveal interpretations while a phase decision is still open.
Facilitator reveal · not participant evidence
Read the logs.
Bound the claim.
A technical answer sheet for checking whether teams found the important clues—and whether they separated observation, inference and uncertainty.
Use the slide for evidence the room actually discussed; skip the rest.
“What does this prove, and what would you need to prove more?”
Use on every record
Seven questions before making a claim
Which system produced the record? CloudTrail, application audit and runtime telemetry see different things.
Event time, ingestion time and enrichment time may differ. Correlation needs a defensible window.
Which human, session, service role, task, node or provider grant performed the action?
Was this a request, an allowed decision, an API call, a process start or a completed result?
Which merchant, workspace, task, file, domain or role was affected?
Success, allowed, HTTP 200 and accepted flow each mean something different.
Which ID links records without relying only on close timestamps?
RB Detect · correlation corr_91d7
Weak alone; useful together
| Time | Signal | Important value |
|---|---|---|
| 09:17:12 | Correlation | 62/100 · LOW |
| 09:15:58 | Credential-form heuristic | 4 of 14 drafts |
| 09:14:03 | Asset burst | 14 in 8 min; median 2/day |
| 09:07:51 | Login context | First-seen IP, device, country |
The score only crossed the alert threshold of 60, so an analyst ticket—not automatic suspension—was proportionate.
“14 assets” matters because this merchant normally creates two per day and had never exceeded four.
Email/password fields and “sign in to claim” language make the burst more concerning than a normal campaign batch.
This supports investigation. It does not prove account takeover, publication or credential theft.
AWS CloudTrail · control plane
Who started which workspace?
"eventTime": "2026-09-12T09:09:58Z",
"eventSource": "ecs.amazonaws.com",
"eventName": "RunTask",
"userIdentity": {"type": "AssumedRole",
"arn": "...assumed-role/rb-campaign-orchestrator-prod/run-2bd41"},
"requestParameters": {
"cluster": "rb-campaign-prod",
"taskDefinition": "rb-campaign-workspace:42",
"tags": [{"merchant_id":"merchant_1047"},
{"workspace_id":"ws_74ac1"}]},
"responseElements": {"failures": []},
"sourceIPAddress": "10.42.7.19"
Service and operation: an ECS control-plane request to start a task—not evidence of commands inside it.
Caller: temporary credentials for the orchestrator role made the AWS API call. The merchant is not the CloudTrail principal.
Target and join keys: task definition plus merchant/workspace tags connect AWS activity to application records.
API outcome: no reported placement failure. Confirm the resulting task ARN/state elsewhere.
Does not show: `apt-get`, Python, file reads or `nmap`. Those require runtime telemetry.
Job manifest + data-access audit · ctx_2201
A preview job inherited delivery data
Model context
brand_profile5 campaign summariesinactive 30 days · approximate count 184No customer identifier or row-level file is listed here.
Workspace files
brand/logo.pngbrief/campaign.jsonaudience/audience_184.csv184 rows: customer hash, last order time and city.
job_mode: asset_previewThe job creates assets; it is not delivering a campaign to individual customers.
included_by: campaign_bundle_v2.default_inputsThe reused delivery-oriented bundle automatically requested the row export.
tenant_filter: merchant_id=merchant_1047Tenant scoping worked, but it does not show that the preview workspace needed the rows.
asset_preview needed member rows. Drive is available, but content_loaded=false and its last file activity predates the run; the application audit identifies the internal services in the CSV path.Agent trace + retrieval gateway + policy
Three allowed steps create one unsafe path
raw.githubusercontent.com/…/render-campaign.pyKnown host ≠ trusted object200 · text/plain · /work/render-campaign.pyOnly type and size inspectedpython /work/render-campaign.pyNo signature or human approvalclassification: campaign_helperA label explains why policy allowed it; a model classification is not a security boundary.
human_approval: falseNo person approved the exact bytes, hash, destination and command.
content_signature_required: falseThe logged SHA-256 supports later correlation, but was not checked against an allowlist before execution.
`web.retrieve` and `process.start` were judged independently. No rule evaluated their data dependency.
Application-emitted events in CloudWatch Logs
The task was ephemeral; the telemetry cannot be
09:26:01 process.start uid=0 python render-campaign.py
09:26:44 process.start uid=0 apt-get install -y nmap nikto
09:27:17 process.start uid=0 nmap -sV 127.0.0.1
09:27:28 file.open uid=0 /etc/shadow · allowed
09:28:41 process.start uid=0 nikto -h 127.0.0.1:3000 -o nikto-results.json
09:28:58 file.write uid=0 /work/nikto-results.json · 18 KB
uid=0 means container rootThe process began with root authority. No lower-to-higher transition is shown, so privilege escalation is not proven.
A writable runtime and package manager turned campaign rendering into a general-purpose testing environment.
/etc/shadow · allowedProves the file open was permitted. It does not show useful hashes, cracking or credentials used elsewhere.
These are application/runtime records shipped to CloudWatch Logs—not CloudTrail events.
Resolver + Flow Logs + GuardDuty + enrichment
Each source answers a different question
promo-assets-cdn.exampleThe workload asked to resolve the name. It does not show response content.
10.42.2.14 → external IP · 443 · ACCEPTA network flow was permitted. It does not identify the application payload.
Trojan:EC2/PhishingDomainRequest!DNSA threat-intelligence match attributes the query to the EC2 node.
phishing · PictoPop Partner AccessIndependent context raises confidence; it is not the source of the GuardDuty finding.
GuardDuty finding anatomy
The finding names the node—not the merchant
"type": "Trojan:EC2/PhishingDomainRequest!DNS",
"severity": 8,
"resourceType": "Instance",
"instanceId": "i-0c84f271a4b91e620",
"privateIpAddress": "10.42.2.14",
"iamInstanceProfile": "rb-ecs-node-prod",
"resourceRole": "ACTOR",
"actionType": "DNS_REQUEST",
"domain": "promo-assets-cdn.example",
"protocol": "UDP",
"blocked": false,
"eventFirstSeen": "2026-09-12T09:26:03Z",
"count": 1
Finding type and score: threat category plus prioritization; neither is a verdict about every action by the resource.
Resource: GuardDuty sees the EC2 container host and its instance profile. Correlate to the task before naming a tenant.
Observed action: the resource acted as the DNS requester for the named domain.
Not blocked: this detector observed the request; it did not enforce DNS denial.
Scope: one observed occurrence at this timestamp. Search surrounding DNS and network records.
EC2 Instance Metadata Service
HTTP 200 proves reachability—not credential theft
GET 169.254.169.254/…200Evidence supports
- Requests reached metadata paths
- HTTP success status was observed
- The isolation design needs review
- Potential node-role exposure must be investigated
Evidence does not support
- Which response body was returned
- That node-role credentials were obtained
- That credentials were used in an AWS API call
- Host-root or lateral movement
Identity and permission boundaries
Four roles; four different jobs
| Role | Who uses it | Representative permissions | Investigation question |
|---|---|---|---|
Orchestratorrb-campaign-orchestrator-prod | Campaign service | ecs:RunTaskiam:PassRole | Can it pass only the exact task and execution roles? |
Execution rolerb-campaign-execution-prod | ECS agent | ecr:Get…logs:CreateLogStream | Is application code able to assume or read this role? |
Task rolerb-campaign-task-prod | Code in the task | bedrock:InvokeModellogs:PutLogEvents | Are resources, actions and conditions scoped to this workload? |
Node rolerb-ecs-node-prod | EC2 container host | ECS host operations | Can a task reach its metadata credentials? |
iam:PassRoledoes not mean “assume the role.” It lets the orchestrator tell ECS which approved role to attach to a task. Scope it by role ARN and service conditions.Task-role policy statement
Read action and resource together
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"s3:GetObject", "s3:PutObject",
"logs:CreateLogStream", "logs:PutLogEvents"
],
"Resource": [
"arn:aws:bedrock:*::foundation-model/*",
"arn:aws:s3:::rb-campaign-artifacts-prod/merchant_1047/*",
"arn:aws:logs:us-east-1:123456789012:log-group:/ecs/rb-campaign-workspace:*"
]
Effect: matching requests are permitted. An explicit deny elsewhere would still win.
Actions: the application may invoke a model, read/write artifacts and emit logs. This statement does not grant ECS task creation.
Broad model resource: every listed foundation model in every region is wider than one approved model/version.
Tenant-scoped prefix: S3 is narrowed to merchant_1047. Ask whether policies are generated per tenant and cannot be confused.
Region/account/log scope: log writes are restricted more tightly than the Bedrock resource.
Google Workspace Drive audit
Persistence becomes a public access path
nikto-results.json
promotion-assets.zip
Anyone with link · viewer
New workspace downloads archive
It survives deletion of the ephemeral workspace and can join uploads/downloads by actor, file and time.
drive.file is narrower—not harmlessThe grant can still access files created or opened through the application.
An upload proves storage. The permission change makes the folder and inherited files readable to anyone holding the link.
The connector exercises the OAuth grant; an anonymous reader later needs only the public link.
Runtime + HTTP + Drive + recipient report
Prove collection and retrieval one event at a time
Campaign files are served from the workspace.
Proves local hostingAn outbound connection exposes the local port publicly.
No inbound security-group rule requiredPage imitates PictoPop and asks for email/password.
Proves public deceptive contentsub_4c81 contains email and password fields.
The submission is written into the public folder.
Proves storage and exposureA link user retrieves the submission from an external IP.
Proves exfiltrationConfirmed chain
POST /collect → submission_id=sub_4c81 → Drive object upload → inherited public permission → anonymous download.
Still avoid claiming
That the production frontend was modified, that other merchants submitted credentials, or that the captured credential was used elsewhere.
Application job record + CloudTrail
The later task is not a second login
"job_id": "job_91c2",
"created_at": "2026-09-12T09:49:42Z",
"created_by_session": "ses_8f21c",
"scheduled_for": "2026-09-12T10:08:00Z",
"started_at": "2026-09-12T10:08:14Z",
"authorization": {
"evaluated_at": "enqueue",
"merchant_state_at_execution": "not_checked"
}
10:05:12 TerminateSession ses_8f21c SUCCESS
10:08:14 CloudTrail RunTask by orchestrator role
Provenance: the job was accepted while the known session was valid. The session ID joins queue and identity records.
Schedule and start: the task began 14 seconds after its scheduled time.
Design gap: authorization was evaluated at enqueue; merchant suspension was not checked at execution.
Sequence: the session ended before the service role started the task.
Conclusion: continuing pre-authorized work, not evidence of a fresh attacker login after containment.
Authority inventory
Containment must follow every authority
Short-lived bearer authority. Terminate session and prevent refresh.
Renewable session authority. Revoke the session family.
Stopping one task does not stop a newly authorized task receiving fresh credentials.
Possible exposure through IMDS; acquisition and use are unproven.
Independent provider authority survived account suspension.
Anonymous read access remains until the anyone-with-link permission is removed, even if OAuth is revoked.
Not a credential: a cached policy result that must be invalidated or re-evaluated.
Evidence confidence
What the incident record can—and cannot—say
✓ Observed
- Valid password-backed merchant session
- GitHub Raw object downloaded and executed
- Root-running task, Nikto scan and shadow-file access
- Drive folder made link-readable
- Credential submission stored and anonymously downloaded
- Queued task started after session termination
≈ Reasonable inference
- The retrieved helper caused the near-immediate callbacks
- The public page was part of the attacker’s phishing objective
- The password was stolen or reused before the first observed login
State these as hypotheses and name the supporting correlation.
? Not proven
- Reconnaissance before login
- Privilege escalation or host compromise
- AWS credentials returned or used
- Use of the stolen merchant credential elsewhere
- Additional victim submissions
- Production frontend compromise
“Which single additional event, field or correlation would most change your confidence—and which team should own producing it next time?”
Keep answers out of the play path
One deck, two deliberate entry points
A prominent “Open technical answer deck” button lets the facilitator prepare or keep it in a separate tab.
The normal sidebar stays focused on roles, phases, architecture and the team’s operating picture.
A second link appears in the facilitator-only debrief, where interpretations can be revealed selectively.
Pick the evidence that generated disagreement or an overconfident claim.
Open in a new tab so the shared game state, decisions and summary remain untouched.
Ask for source, actor, result and limitation before revealing the annotations.