Smoke Test: Credential Playbooks + Reporting¶
End-to-end walkthrough of the credential playbooks (A.1–A.4) and reporting deliverable (B.1–B.4) shipped in the credential-driven E2E plan. Use this to dogfood the flow before running it on a real engagement.
The smoke seed pre-populates the graph with five captured credentials (AWS, GitHub PAT, CI/CD OIDC, Entra refresh + access tokens), the supporting federation graph, an objective targeting an AWS PowerUser role, and an ADMIN_TO edge so the attack-path analyzer has a starting point. Cross-tier inference fires during seed, so an inferred ASSUMES_ROLE edge is already in place — the playbooks confirm it through replay rather than discovering it from scratch.
No real cloud APIs are called. All commands return canned fixtures the operator pipes through parse_output.
0. Prerequisites¶
For the optional PDF check:
# Linux / Kali:
apt-get install chromium
# macOS: Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome works.
1. Seed the engagement¶
You should see:
Smoke engagement seeded:
Engagement dir: ./smoke-engagement/
Config file: smoke-engagement/config.json
State file: smoke-engagement/state.json
Engagement id: smoke-credential-playbooks
Seeded credentials:
cred-aws-power AWS poweruser temp creds (oidc_access_token shape)
cred-gh-pat GitHub PAT for svc-deploy
cred-oidc-gha Captured GHA OIDC token (audience: sts.amazonaws.com)
cred-entra-rt Entra refresh token for alice@acme.local
cred-entra-at Entra access token for alice@acme.local (post-MFA)
2. Start the MCP server pointed at the smoke engagement¶
OVERWATCH_CONFIG=smoke-engagement/config.json \
OVERWATCH_STATE_FILE=smoke-engagement/state.json \
node dist/index.js
The server starts on stdio (for MCP) and the dashboard on http://localhost:8384.
3. Open the dashboard¶
http://localhost:8384
Confirm before going further:
- Sidebar shows the new Findings tab (ShieldAlert icon, between Attack Paths and Settings).
- AttackPaths tab lists at least one path: host-jumpbox → cred-oidc-gha → cloud-id-poweruser.
- Identity tab shows the acme-corp (github_org) and acme.onmicrosoft.com (entra) IdPs with their applications.
- Findings tab is initially empty (no parsed findings yet) and shows the "Generate Report" button.
If any of these are missing, hard-refresh the page (Cmd-Shift-R / Ctrl-F5) — Vite serves hashed JS bundles and a stale index.html keeps loading old assets.
4. Walk the AWS playbook¶
In your MCP-connected Claude Code session (or via any MCP client):
You should get back a 6-step plan starting with aws sts get-caller-identity. The credential should now be stamped with recon_playbook_invoked_at (re-load the dashboard's Identity tab; the credential row reflects this).
Now drive step 1 with a canned response. Pipe the fixture below through parse_output:
Fixture: aws sts get-caller-identity output
{
"UserId": "AROAEXAMPLE:smoke-session",
"Account": "111122223333",
"Arn": "arn:aws:sts::111122223333:assumed-role/PowerUser/smoke-session"
}
Call:
{ "tool": "parse_output",
"args": {
"tool_name": "aws-sts-identity",
"raw_output": "<paste the JSON above>",
"agent_id": "smoke-aws",
"context": { "source_credential_id": "cred-aws-power" }
}}
Expected: a new cloud_identity for the assumed-role session lands in the graph; an OWNS_CRED edge connects it to cred-aws-power. Refresh the Identity tab — the new principal appears.
Fixture: aws iam get-account-summary
{
"SummaryMap": {
"Users": 12, "Groups": 4, "Roles": 30, "Policies": 17,
"AccountMFAEnabled": 1, "MFADevices": 8, "AccessKeysPerUserQuota": 2
}
}
Pipe through parse_output with tool_name: "aws-iam-summary" and context: { aws_account: "111122223333" }.
Expected: a synthesized account-root cloud_identity carries account_summary + account_summary_observed_at.
(The remaining steps —list-attached-*-policies, cloudfox, s3 list-buckets, lambda list-functions — follow the same pattern. Skip them for the smoke unless you want to see the inventory parser populate.)
5. Walk the GitHub playbook¶
{ "tool": "expand_github_credential", "args": { "credential_id": "cred-gh-pat", "include_orgs": true } }
Returns a 3-step plan (validate → /user/orgs → /user/repos). Pre-expand a single repo for full coverage:
{ "tool": "expand_github_credential",
"args": { "credential_id": "cred-gh-pat", "candidate_repos": ["acme-corp/webapp"] } }
You'll get the 4 per-repo steps too (secrets, branch protection, deploy keys, OIDC trust customization).
Fixture: gh api /user/orgs --paginate
parse_output with tool_name: "gh-api-orgs", context { source_credential_id: "cred-gh-pat" }.
Fixture: gh api /repos/acme-corp/webapp/branches/main/protection (unprotected)
parse_output with tool_name: "gh-api-branch-protection", context { repo_full_name: "acme-corp/webapp", branch_name: "main" }.
Expected: the acme-corp/webapp idp_application gets stamped with branch_protection_gaps: [...] and finding_severity: "high". Visit Findings tab and refresh — a new high-severity finding should appear once /api/findings next polls (8s) or after you click around.
Fixture: gh api /repos/acme-corp/webapp/keys
[
{ "id": 1, "key": "ssh-rsa AAAAFAKEKEY1...write", "title": "ci-deploy", "read_only": false, "created_at": "2025-08-01T00:00:00Z" }
]
parse_output with tool_name: "gh-api-deploy-keys", context { repo_full_name: "acme-corp/webapp" }.
Expected: new credential node <...ci-deploy> with deploy_key_write_access: true and finding_severity: "high".
6. Walk the OIDC capture playbook¶
The smoke seed already has the federation graph in place, so you should see:
candidates_considered: 1
step_count: 1
steps: [{ tool: "validate_token_credential", args: { ..., target_role_arn: "arn:aws:iam::111122223333:role/PowerUser" }, ... }]
If candidates_considered: 0, the seed didn't run — re-run step 1.
You don't have to actually run the replay (no real STS endpoint). The plan-output is what the playbook ships.
7. Walk the Entra playbook¶
{ "tool": "exchange_refresh_token",
"args": { "credential_id": "cred-entra-rt", "client_id": "1950a258-227b-4e31-a9cf-717495945fc2" } }
Returns a single curl POST step against https://login.microsoftonline.com/acme.onmicrosoft.com/oauth2/v2.0/token. The refresh-token value is referenced via $REFRESH_TOKEN; check payload.command does not contain the literal value.
{ "tool": "expand_entra_credential",
"args": { "credential_id": "cred-entra-at", "include_groups": true } }
Returns a 5-step plan (/me, /users, /applications, /servicePrincipals, /groups).
Fixture: /v1.0/users?$top=999
{
"value": [
{ "id": "u-1", "userPrincipalName": "alice@acme.local", "displayName": "Alice", "mail": "alice@acme.local", "accountEnabled": true },
{ "id": "u-2", "userPrincipalName": "bob@acme.local", "displayName": "Bob", "accountEnabled": true }
]
}
parse_output with tool_name: "msgraph-users", context { tenant_id: "acme.onmicrosoft.com" }.
Fixture: /v1.0/servicePrincipals (with high-priv scope to trigger CONSENT_ABUSE)
{
"value": [
{
"id": "sp-priv", "appId": "app-priv", "displayName": "Risky-Consent-App",
"servicePrincipalType": "Application",
"oauth2PermissionScopes": [
{ "id": "s1", "value": "Mail.ReadWrite", "type": "User" },
{ "id": "s2", "value": "Files.ReadWrite.All", "type": "Admin" }
],
"appOwnerOrganizationId": "external-tenant"
}
]
}
parse_output with tool_name: "msgraph-serviceprincipals", context { tenant_id: "acme.onmicrosoft.com" }.
Note: CONSENT_ABUSE requires assigned_user_count >= 10 to fire. The fixture above only seeds the app — to trigger the rule, separately add assigned_user_count: 25 via a report_finding updating the same node, or run the assigned_user_count-aware path. For the smoke, just confirm the SP node lands with app_kind: "entra_service_principal" and external_app: true.
8. Generate the report¶
In the dashboard, click Findings → Generate Report. The modal lets you pick:
- Format: start with
markdown(fast smoke). Trypdfif you have chromium installed. - Theme:
lightfor HTML/PDF. - Client-safe redaction: off for the operator pass; on for the second pass.
- Include Attack Paths: on (default).
- Include compliance mapping: on.
- Include retrospective: off (not relevant for smoke).
Click Render & Save. After ~1 sec, the modal closes and the Reports archive section under the findings list shows the new entry. Click Download to verify the file.
What to look for in the markdown:
1. ## Findings Summary — counts reflect everything you ingested.
2. ## Attack Paths — should contain a numbered chain:
1. jumpbox.acme.local `(host)`
→ `OWNS_CRED` (confirmed, conf 1.00)
2. gha-oidc-token-prod-deploy `(credential)`
→ `ASSUMES_ROLE` (inferred by `oidc_federation_pivot`, conf 0.75)
3. arn:aws:iam::111122223333:role/PowerUser `(cloud_identity)`
## Credential Chains (if step 4 added a temp cred via parse).
4. ## Recommendations — touches the high-severity findings the GitHub deploy-key + branch-protection parsers stamped.
For PDF: download the .pdf, open it. Should be a styled multi-page render. If you get an error like No Chromium / Chrome binary found, install one (see prerequisites) or fall back to HTML.
9. Sanity checks (what should be true at the end)¶
Run via direct curl (no MCP needed):
# Findings populated
curl -s http://localhost:8384/api/findings | jq '.severity_summary, .total'
# Reports archive shows your render
curl -s http://localhost:8384/api/reports | jq '.total, .reports[0]'
# Engagement state has all 5 credentials
curl -s http://localhost:8384/api/state | jq '.state.graph_summary.nodes_by_type.credential'
# Attack paths panel has the OIDC pivot chain
curl -s http://localhost:8384/api/paths/obj-aws-admin | jq '.count, .paths[].nodes'
Expected:
- severity_summary shows high >= 2 (deploy-key + branch-protection) once the GitHub steps ran.
- reports.total >= 1.
- nodes_by_type.credential is at least 5 (more if you ran the deploy-keys + secrets fixtures).
- paths returns at least one chain.
10. What to flag back if anything looks rough¶
- Plan steps that reference parsers / context fields that don't exist
- Approval prompts for steps that should auto-approve (or vice versa)
- Findings that the classifier mis-categorizes
- Attack-path chains rendered with wrong direction or missing edges
- PDF formatting issues (missing sections, broken page breaks, Unicode)
- Anything you expected to be in the report but isn't
Once the smoke passes cleanly here, the same flow works against a real engagement — just swap the fixture JSON for actual aws ... / gh api ... / curl ... graph.microsoft.com outputs.