critical Threat analysis

Miasma DurableTask GitHub Repository Compromise

On June 5, 2026, the official Azure/durabletask GitHub repository was compromised. Threat actors pushed a backdated commit ('Switched DataConverter to OrchestrationContext [skip ci]') that added a malicious tasks.json and configuration files targeting AI coding tools to execute credential-stealing payloads.

#github#repository-compromise#supply-chain#credential-theft#microsoft#azure#miasma#hades#teampcp
On this page 0% read

    Executive Summary

    On 2026-06-05, security researchers disclosed a highly sophisticated supply chain compromise targeting the official Microsoft/Azure repository Azure/durabletask [Source 2]. A compromised contributor account was used to push a backdated commit titled "Switched DataConverter to OrchestrationContext [skip ci]" [Source 2]. The commit introduced zero functional changes to the codebase but added .vscode/tasks.json with "runOn": "folderOpen" to execute .github/setup.js automatically when the folder is opened in Visual Studio Code [Source 1] [Source 4].

    Furthermore, the commit planted configuration files targeting AI coding assistants (Claude Code, Cursor, Gemini CLI) to execute credential-stealing payloads [Source 2] [Source 4]. Microsoft and GitHub security teams responded within minutes, disabling 73 repositories across 4 organizations (Azure, Azure-Samples, Microsoft, MicrosoftDocs) to contain the spread [Source 1] [Source 3]. The compromised account was linked to prior activity on PyPI where it published malicious versions of the durabletask package (1.4.1, 1.4.2, 1.4.3) [Source 2].

    Key Facts

    threat_type: "Repository Compromise, AI Coding Assistant Abuse & Credential Theft"
    ecosystem: "github, git, vscode, claude, cursor, gemini"
    registry: "GitHub"
    affected_repositories:
      - "Azure/durabletask"
    compromised_files:
      - ".vscode/tasks.json"
      - ".claude/settings.json"
      - ".gemini/settings.json"
      - ".cursor/rules/setup.mdc"
      - ".github/setup.js"
    reported_publish_date: "2026-06-05"
    execution_trigger: "Opening the repository in VS Code or using AI coding assistants (Claude Code, Cursor, Gemini CLI) in the workspace"
    publish_path: "Compromised contributor credentials (PAT)"
    credential_risk:
      - "AWS credentials"
      - "Azure service principal tokens"
      - "Google Cloud tokens"
      - "Kubernetes config files"
      - "GitHub personal access tokens"
      - "Developer workstation environments"

    Source Confidence and Claim Ledger

    ClaimStatusEvidence
    The Azure/durabletask repository was compromised on June 5, 2026.confirmedThreatLocker and StepSecurity report that a compromised contributor account pushed the malicious commit [Source 1] [Source 2].
    The commit added tasks.json running a payload on folder open in VS Code.confirmedStepSecurity and Zscaler detail the .vscode/tasks.json configuration utilizing "runOn": "folderOpen" to invoke .github/setup.js [Source 2] [Source 4].
    The commit targeted AI coding tools (Claude Code, Cursor, Gemini CLI).confirmedPhoenix Security and Zscaler document the inclusion of .cursor/rules/setup.mdc, .claude/settings.json, and .gemini/settings.json [Source 3] [Source 4].
    GitHub disabled 73 repositories across Azure and Microsoft organizations to contain the spread.confirmedThreatLocker and Phoenix Security report that GitHub swept and disabled 73 repositories across 4 organizations (Azure, Azure-Samples, Microsoft, MicrosoftDocs) [Source 1] [Source 3].
    The compromise is linked to prior durabletask PyPI malicious packages.confirmedStepSecurity confirmed the same contributor account was responsible for publishing malicious durabletask versions 1.4.1, 1.4.2, and 1.4.3 on PyPI [Source 2].

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decisionClosure condition
    Confirmed compromiseAn affected repository was cloned, and opened in VS Code (with auto-run tasks enabled) or accessed via Cursor, Claude Code, or Gemini CLI, and telemetry shows outbound requests to C2 domains or executions of .github/setup.js.Terminal command history, VS Code task logs, process execution telemetry (e.g. node.js running setup.js), network logs showing connections to getsessions.org or masscan.cloud.Isolate the developer workstation, revoke and rotate all credentials stored on the workstation (AWS, Azure, GCP, GitHub), and check for lateral movement.Workstation is re-imaged, all credentials are confirmed rotated, and network/EDR monitoring shows no further C2 beaconing.
    Presumed exposedAn affected repository was cloned and opened in a workspace with active AI coding assistants or VS Code, but endpoint/network logs are incomplete.Git clone history, workspace file paths, or developer shell history containing references to the compromised durabletask repository branches.Revoke and rotate all developer and cloud tokens accessible from the workstation.Rebuild clean environments and confirm revocation of all potentially exposed API keys.
    Potentially exposedThe repository was cloned, but it was never opened in VS Code or accessed via targeted AI tools.Git clone records, workspace directory inventory, and shell logs.Audit the local workspace directory to confirm the absence of execution triggers, or delete the cloned repository safely.Clean deletion of the workspace folders.
    Not exposedThe repository was not cloned, or local Git repositories do not contain the compromised commit or branch.Negative search results in developer environment inventories and codebase scanning.Maintain standard prevention controls (disable task auto-run in VS Code).Search evidence covers all active developer endpoints.
    UnknownInventory, shell history, or network/EDR logs are missing.A gap statement naming unavailable systems or logs.Treat the system as potentially exposed and conduct a targeted audit.Gaps are resolved or risk accepted.

    Minimum Evidence To Collect

    minimum_evidence:
      - "Developer workspace inventory checking for the existence of .github/setup.js or tasks.json with runOn folderOpen"
      - "EDR process execution logs for node.js executing setup.js"
      - "Network/DNS resolution logs for getsession.org, masscan.cloud, or git-tanstack.com"
      - "Git logs or checkout histories for durabletask repository showing commit Switched DataConverter to OrchestrationContext"

    Timeline

    • 2026-06-05T08:00:00Z: The attacker utilizes compromised contributor PAT credentials to push a backdated commit (“Switched DataConverter to OrchestrationContext [skip ci]”) to the official Azure/durabletask repository. The commit is backdated to March 9, 2020. Source: [Source 2]
    • 2026-06-05T08:15:00Z: StepSecurity and ThreatLocker automated monitoring systems flag an anomalous change in repository config files targeting IDE and AI agent settings. Source: [Source 1] [Source 2]
    • 2026-06-05T08:20:00Z: GitHub security systems perform an automated sweep and disable 73 repositories across Azure, Azure-Samples, Microsoft, and MicrosoftDocs organizations to prevent local auto-run spread. Source: [Source 1] [Source 3]
    • 2026-06-05T10:30:00Z: Security researchers publish analysis linking the compromise to the broader Miasma/Hades campaign run by TeamPCP. Source: [Source 3] [Source 4]

    What Happened

    On June 5, 2026, threat actors targeted the developer ecosystem by exploiting developer configurations instead of standard runtime dependencies [Source 2] [Source 4]. Utilizing compromised Personal Access Tokens (PATs) belonging to an Azure project contributor, the attackers pushed a backdated commit to the Azure/durabletask repository [Source 2]. The commit message “Switched DataConverter to OrchestrationContext [skip ci]” was selected to bypass automated CI/CD builds [Source 2]. It did not modify any source code files but added five files designed to trigger a large, obfuscated JavaScript payload (.github/setup.js) when opened in VS Code or various AI-assisted coding tools [Source 4].

    Recognizing the threat, GitHub took immediate automated action, disabling 73 repositories across multiple Microsoft organizations to isolate the backdoor [Source 1]. This threat is tracked as a child event of the Miasma worm (Hades variant) operated by TeamPCP, who had previously hijacked PyPI credentials to publish backdoored durabletask packages [Source 2] [Source 3].

    Technical Analysis

    Initial Access

    The attacker gained write access to the Azure/durabletask repository by using a compromised contributor’s Personal Access Token (PAT), bypassing standard OIDC/trusted commit validation gates [Source 2].

    Package or Artifact Manipulation

    The commit backdated the author/committer timestamps to March 9, 2020, to make the changes appear historical and evade Git history checks [Source 2]. Rather than modifying actual logic, it introduced:

    • .github/setup.js (4.6 MB obfuscated JavaScript file)
    • .vscode/tasks.json
    • .claude/settings.json
    • .gemini/settings.json
    • .cursor/rules/setup.mdc

    Execution Trigger

    The attack leveraged the automatic execution features of modern IDEs and AI coding tools [Source 4]:

    • VS Code: The "runOn": "folderOpen" option in .vscode/tasks.json automatically runs the configured task (executing node .github/setup.js) as soon as the workspace folder is opened by a user.
    • Claude Code & Gemini CLI: The SessionStart hook in .claude/settings.json and .gemini/settings.json executes the script when the AI session begins.
    • Cursor: The .cursor/rules/setup.mdc file uses prompt injection to instruct the Cursor AI agent to execute .github/setup.js under the guise of setting up the workspace.

    Payload Behavior

    Once triggered, .github/setup.js scans the local developer workstation or runner. It targets active environment variables, credentials, configuration profiles, and files containing secret keys for AWS, Azure, Google Cloud, Kubernetes (kubeconfig), and GitHub.

    Exfiltration / C2

    Collected credentials are sent to TeamPCP-controlled C2 servers [Source 4]:

    • api.masscan.cloud
    • filev2.getsession.org
    • seed1.getsession.org
    • seed2.getsession.org
    • seed3.getsession.org
    • git-tanstack.com

    Propagation

    The malware does not feature direct replication code inside durabletask, but stolen tokens are routinely recycled by TeamPCP’s centralized infrastructure to automate compromises of other packages downstream [Source 2].

    Obfuscation or Evasion

    The setup.js file is heavily obfuscated with nested eval structures and anti-analysis checks, and the [skip ci] flag in the commit prevented CI/CD pipelines from running tests that might have triggered detections.

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "github"
        - "vscode"
      packages: []
      repositories:
        - "Azure/durabletask"
      container_images: []
      CI_CD_systems:
        - "GitHub Actions"
      developer_tools:
        - "Visual Studio Code"
        - "Claude Code"
        - "Cursor IDE"
        - "Gemini CLI"
    credentials_at_risk:
      - AWS access keys
      - Azure service principal tokens
      - Google Cloud credentials
      - GitHub personal access tokens
      - Kubernetes configuration files (kubeconfig)

    Indicators of Compromise

    domains:
      - value: "api.masscan.cloud"
      - value: "filev2.getsession.org"
      - value: "seed1.getsession.org"
      - value: "seed2.getsession.org"
      - value: "seed3.getsession.org"
      - value: "git-tanstack.com"
    urls:
      - value: "hxxps://api.masscan.cloud"
      - value: "hxxps://filev2.getsession.org"
      - value: "hxxps://seed1.getsession.org"
      - value: "hxxps://seed2.getsession.org"
      - value: "hxxps://seed3.getsession.org"
      - value: "hxxps://git-tanstack.com"
    files:
      - value: ".vscode/tasks.json"
      - value: ".claude/settings.json"
      - value: ".gemini/settings.json"
      - value: ".cursor/rules/setup.mdc"
      - value: ".github/setup.js"

    Detection and Hunting

    Script: local repository and exported telemetry scope

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    ROOT = sys.argv[1] if len(sys.argv) > 1 else "."
    LOG_ROOT = os.environ.get("LOG_ROOT", "")
    OUT = Path(os.environ.get("OUT", "hp-miasma-durabletask-github-compromise-scope"))
    SINCE = "2026-06-05T08:00:00Z"
    UNTIL = "2026-06-05T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    FILES = [
      ".vscode/tasks.json",
      ".claude/settings.json",
      ".gemini/settings.json",
      ".cursor/rules/setup.mdc",
      ".github/setup.js",
    ]
    DOMAINS = [
      "api.masscan.cloud",
      "filev2.getsession.org",
      "seed1.getsession.org",
      "seed2.getsession.org",
      "seed3.getsession.org",
      "git-tanstack.com",
    ]
    URLS = [
      "https://api.masscan.cloud",
      "https://filev2.getsession.org",
      "https://seed1.getsession.org",
      "https://seed2.getsession.org",
      "https://seed3.getsession.org",
      "https://git-tanstack.com",
    ]
    IPS = [
    ]
    HASHES = [
    ]
    PROCESS_PATTERNS = [
    ]
    NETWORK_PATTERNS = [
    ]
    
    # Positive signal: repository, lockfile, artifact, process, or network telemetry contains one of the exact incident selectors above.
    # Escalation: any match tied to a production build, CI run, deployed asset, or secret-bearing host moves the asset to presumed exposed.
    
    OUT.mkdir(parents=True, exist_ok=True)
    indicators_file = OUT / "indicators.txt"
    
    # Collect unique indicators
    indicators = set()
    for group in [PACKAGES, VERSIONS, FILES, DOMAINS, URLS, IPS, HASHES, PROCESS_PATTERNS, NETWORK_PATTERNS]:
        for val in group:
            if val:
                indicators.add(val)
    
    with open(indicators_file, "w") as f:
        for ind in sorted(indicators):
            f.write(ind + "\n")
    
    print(f"[+] Written unique selectors to {indicators_file}")
    
    # Walk local directory
    print(f"[+] Scanning directory: {ROOT} for selectors...")
    matches = []
    exclude_dirs = {"node_modules", "vendor", "dist", ".git"}
    for root, dirs, filenames in os.walk(ROOT):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for filename in filenames:
            filepath = Path(root) / filename
            try:
                content = filepath.read_text(errors="ignore")
                for ind in indicators:
                    if ind in content:
                        matches.append(f"{filepath}: found '{ind}'")
            except Exception:
                pass
    
    if matches:
        (OUT / "repository-indicator-matches.txt").write_text("\n".join(matches) + "\n")
        print(f"[!] Found {len(matches)} matches in codebase!")
    
    # Optional Log Scanning
    if LOG_ROOT and os.path.exists(LOG_ROOT):
        print(f"[+] Scanning telemetry log directory: {LOG_ROOT}...")
        log_matches = []
        for root, _, filenames in os.walk(LOG_ROOT):
            for filename in filenames:
                filepath = Path(root) / filename
                try:
                    content = filepath.read_text(errors="ignore")
                    for ind in indicators:
                        if ind in content:
                            log_matches.append(f"{filepath}: found '{ind}'")
                except Exception:
                    pass
        if log_matches:
            (OUT / "exported-telemetry-indicator-matches.txt").write_text("\n".join(log_matches) + "\n")
            print(f"[!] Found {len(log_matches)} matches in logs!")
    
        if PACKAGES:
            registry_dir = OUT / "registry"
            registry_dir.mkdir(exist_ok=True)
    
    print(f"[+] Wrote scope artifacts under {OUT}")

    Downstream Abuse Audits

    Script: GitHub organization run, release, secret, and workflow audit

    #!/usr/bin/env python3
    import os
    import sys
    import json
    import subprocess
    from pathlib import Path
    
    if "ORG" not in os.environ:
        print("ERROR: Set ORG environment variable to the GitHub organization to audit", file=sys.stderr)
        sys.exit(1)
    
    ORG = os.environ["ORG"]
    SINCE = "2026-06-05T08:00:00Z"
    UNTIL = "2026-06-05T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-miasma-durabletask-github-compromise-github-audit"))
    
    SELECTORS = [
      ".vscode/tasks.json",
      ".claude/settings.json",
      ".gemini/settings.json",
      ".cursor/rules/setup.mdc",
      ".github/setup.js",
      "api.masscan.cloud",
      "filev2.getsession.org",
      "seed1.getsession.org",
      "seed2.getsession.org",
      "seed3.getsession.org",
      "git-tanstack.com",
      "https://api.masscan.cloud",
      "https://filev2.getsession.org",
      "https://seed1.getsession.org",
      "https://seed2.getsession.org",
      "https://seed3.getsession.org",
      "https://git-tanstack.com",
    ]
    
    # Positive signal: a workflow run, release, secret, key, package, or workflow change overlaps the exposure window and references an incident selector.
    # Remediation trigger: unauthorized post-exposure write activity or a secret-bearing run matching an incident selector requires token revocation and downstream cloud/registry review.
    
    OUT.mkdir(parents=True, exist_ok=True)
    (OUT / "runs").mkdir(exist_ok=True)
    (OUT / "logs").mkdir(exist_ok=True)
    (OUT / "repos").mkdir(exist_ok=True)
    
    # 1. Write incident-selectors file
    selectors_file = OUT / "incident-selectors.txt"
    with open(selectors_file, "w") as sf:
        for s in SELECTORS:
            if s:
                sf.write(s + "\n")
    
    # 2. Get list of repos
    print(f"[+] Fetching repositories for organization: {ORG}")
    repo_res = subprocess.run(["gh", "repo", "list", ORG, "--limit", "1000", "--json", "nameWithOwner"], capture_output=True, text=True)
    if repo_res.returncode != 0:
        print(f"[-] Failed to fetch repos: {repo_res.stderr}", file=sys.stderr)
        sys.exit(1)
    
    repos = [r["nameWithOwner"] for r in json.loads(repo_res.stdout)]
    
    for repo in repos:
        safe_repo = repo.replace("/", "__")
        print(f"[+] Auditing repository: {repo}")
        
        # Check runs in the window
        runs_res = subprocess.run([
            "gh", "api", f"/repos/{repo}/actions/runs",
            "-f", "per_page=100",
            "-f", f"created=>={SINCE}",
            "--paginate"
        ], capture_output=True, text=True)
        
        if runs_res.returncode == 0:
            try:
                all_runs = json.loads(runs_res.stdout).get("workflow_runs", [])
                filtered_runs = [r for r in all_runs if r["created_at"] <= UNTIL]
                
                if filtered_runs:
                    with open(OUT / "runs" / f"{safe_repo}-runs.jsonl", "w") as rf:
                        for run in filtered_runs:
                            rf.write(json.dumps(run) + "\n")
                            
                            # Fetch log dynamically
                            run_id = str(run["id"])
                            log_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--log"], capture_output=True, text=True)
                            if log_res.returncode == 0:
                                (OUT / "logs" / f"{safe_repo}-{run_id}.log").write_text(log_res.stdout)
                                
                            # Fetch details
                            view_res = subprocess.run(["gh", "run", "view", run_id, "--repo", repo, "--json", "databaseId,workflowName,headSha,event,createdAt,jobs"], capture_output=True, text=True)
                            if view_res.returncode == 0:
                                (OUT / "runs" / f"{safe_repo}-{run_id}.json").write_text(view_res.stdout)
            except Exception as e:
                print(f"[-] Error parsing runs for {repo}: {e}")
    
        # Check releases in window
        subprocess.run(["gh", "api", f"/repos/{repo}/releases", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check repo secrets updated in window
        subprocess.run(["gh", "api", f"/repos/{repo}/actions/secrets", "-f", "per_page=100", "--paginate"], capture_output=True)
        # Check deploy keys
        subprocess.run(["gh", "api", f"/repos/{repo}/keys", "-f", "per_page=100", "--paginate"], capture_output=True)
    
    # Scan output directory for any indicator selector matches
    print("[+] Scanning gathered telemetry for indicator matches...")
    subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(selectors_file), str(OUT)], capture_output=False)
    
    print(f"[+] Wrote GitHub audit artifacts under {OUT}")

    Script: cloud OIDC and deployment credential follow-on audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-05T08:00:00Z"
    UNTIL = "2026-06-05T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-miasma-durabletask-github-compromise-cloud-audit"))
    AWS_REGIONS = os.environ.get("AWS_REGIONS", "us-east-1").split(",")
    
    # Positive signal: token exchange or privileged write activity occurs in the exposure window from GitHub, CI/CD, package registry, or deployment automation identity.
    # Remediation trigger: unexpected write, deploy, IAM, secret, or registry activity tied to an exposed CI/CD path requires trust-policy disablement and credential rotation.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    # 1. AWS CloudTrail Audit
    print("[+] Querying AWS CloudTrail for Web Identity token exchanges...")
    aws_events = []
    for region in AWS_REGIONS:
        res = subprocess.run([
            "aws", "cloudtrail", "lookup-events",
            "--region", region,
            "--start-time", SINCE,
            "--end-time", UNTIL,
            "--lookup-attributes", "AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity",
            "--output", "json"
        ], capture_output=True, text=True)
        
        if res.returncode == 0:
            try:
                events = json.loads(res.stdout).get("Events", [])
                for event in events:
                    ct = json.loads(event.get("CloudTrailEvent", "{}"))
                    ct["region"] = region
                    aws_events.append(ct)
            except Exception as e:
                print(f"[-] Error parsing AWS CloudTrail events: {e}")
    
    if aws_events:
        with open(OUT / "aws-assume-role-with-web-identity.jsonl", "w") as f:
            for ev in aws_events:
                f.write(json.dumps(ev) + "\n")
                
        # Audit follow-on events for returned access keys
        for ev in aws_events:
            access_key = ev.get("responseElements", {}).get("credentials", {}).get("accessKeyId")
            region = ev.get("region", "us-east-1")
            if access_key:
                print(f"[+] Enumerating AWS events for AccessKey: {access_key}")
                f_res = subprocess.run([
                    "aws", "cloudtrail", "lookup-events",
                    "--region", region,
                    "--start-time", SINCE,
                    "--end-time", UNTIL,
                    "--lookup-attributes", f"AttributeKey=AccessKeyId,AttributeValue={access_key}",
                    "--output", "json"
                ], capture_output=True, text=True)
                if f_res.returncode == 0:
                    try:
                        f_events = json.loads(f_res.stdout).get("Events", [])
                        with open(OUT / "aws-follow-on-api-calls.jsonl", "a") as ff:
                            for fe in f_events:
                                ff.write(fe.get("CloudTrailEvent", "{}") + "\n")
                    except Exception as e:
                        print(f"[-] Error writing follow-on events: {e}")
    
    # 2. Azure Activity Log Audit
    print("[+] Querying Azure activity logs...")
    az_res = subprocess.run([
        "az", "monitor", "activity-log", "list",
        "--start-time", SINCE,
        "--end-time", UNTIL,
        "--query", "[?contains(operationName.value, 'write') || contains(operationName.value, 'delete') || contains(operationName.value, 'Microsoft.ManagedIdentity')]",
        "-o", "json"
    ], capture_output=True, text=True)
    
    if az_res.returncode == 0:
        (OUT / "azure-write-delete-activity.json").write_text(az_res.stdout)
    
    # 3. GCP Logging Audit
    print("[+] Querying GCP Cloud Logging...")
    gcp_filter = f'timestamp>="{SINCE}" AND timestamp<="{UNTIL}" AND (protoPayload.methodName="google.sts.v1.SecurityTokenService.ExchangeToken" OR protoPayload.methodName:"GenerateAccessToken" OR protoPayload.methodName:"CreateServiceAccountKey" OR protoPayload.methodName:"SetIamPolicy")'
    gcp_res = subprocess.run([
        "gcloud", "logging", "read", gcp_filter,
        "--format", "json"
    ], capture_output=True, text=True)
    
    if gcp_res.returncode == 0:
        (OUT / "gcp-token-and-iam-activity.json").write_text(gcp_res.stdout)
    
    print(f"[+] Wrote cloud audit artifacts under {OUT}")

    Script: registry metadata and artifact audit

    #!/usr/bin/env python3
    import os
    import json
    import subprocess
    from pathlib import Path
    
    SINCE = "2026-06-05T08:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-miasma-durabletask-github-compromise-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
    ]
    
    # Positive signal: workflow files or extensions reference the affected action/extension names or versions.
    # Remediation trigger: exposed secrets or OIDC federation policies must be immediately rotated.
    
    OUT.mkdir(parents=True, exist_ok=True)
    
    with open(OUT / "affected-versions.txt", "w") as av:
        for version in VERSIONS:
            if version:
                av.write(version + "\n")
    
        # 1. Search local workspace files for the affected actions/extensions
        print("[+] Scanning workspace workflows for selectors...")
        for file in Path(".").glob(".github/workflows/**/*.yml"):
            subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), str(file)])
    
        # 2. HOW TO ROTATE EXPOSED GITHUB ACTIONS SECRETS:
        # Overwrite compromised secrets with newly generated credentials:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--body", "my-new-secret-value", "--repo", "my-org/my-repo"])
        # For organization-level secrets:
        # subprocess.run(["gh", "secret", "set", "COMPROMISED_SECRET_NAME", "--org", "my-org", "--visibility", "private"])
        # Revoke compromised OIDC federated trust credentials in AWS/GCP and redeploy the IAM trust policy:
        # subprocess.run(["aws", "iam", "update-assume-role-policy", "--role-name", "my-role-name", "--policy-document", "file://new-clean-trust-policy.json"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Actionable SOC/IR Hunt Recipes

    KQL Queries (Microsoft Sentinel / Defender)

    // Detect node.js running setup.js from .github directory
    DeviceProcessEvents
    | where ProcessCommandLine has "node" and ProcessCommandLine has ".github/setup.js"
    | project TimeGenerated, DeviceName, InitiatingProcessAccountName, ProcessCommandLine, FolderPath
    // Detect network connections to Miasma C2 domains
    DeviceNetworkEvents
    | where RemoteUrl has_any ("masscan.cloud", "getsession.org", "git-tanstack.com")
    | project TimeGenerated, DeviceName, LocalIP, RemoteIP, RemoteUrl, RemotePort, InitiatingProcessCommandLine

    Sigma Rules

    title: Miasma Auto-Run Task Execution
    id: 9a3e210b-85cd-4b82-938b-d72b5368a529
    status: experimental
    description: Detects execution of setup.js from a .github directory, which is a key indicator of the Miasma/Hades campaign targeting developer environments.
    author: Halting Problems Threat Intel
    date: 2026-06-05
    references:
      - https://haltingproblems.com/analysis/miasma-durabletask-github-compromise/
    logsource:
      category: process_creation
      product: windows
    detection:
      selection:
        Image|endswith:
          - '\node.exe'
          - '\node'
        CommandLine|contains:
          - '.github/setup.js'
          - 'setup.js'
      condition: selection
    falsepositives:
      - Legitimate repository setup scripts named setup.js
    severity: high

    Shell Hunting Checks

    # Scan for VS Code tasks.json files configured to run on folder open
    find . -name "tasks.json" -path "*/.vscode/*" -exec grep -l '"runOn": *"[fF]olderOpen"' {} \;
    
    # Scan for AI assistant settings files
    find . -type f \( -path "*/.claude/settings.json" -o -path "*/.gemini/settings.json" -o -path "*/.cursor/rules/*.mdc" \)

    Repository Scanner for Malicious AI and Task Configurations

    Below is the complete, runnable Python script that can scan a directory of repositories to find tasks.json files using "runOn": "folderOpen" or suspicious AI configuration files (.claude, .cursor) calling scripts. Save this script as miasma_durabletask_github_audit.py and run it against your code repositories.

    #!/usr/bin/env python3
    """Audit repositories and workspaces for Miasma/Hades AI Coding Assistant and VS Code task compromise."""
    
    from __future__ import annotations
    
    import argparse
    import json
    import re
    import sys
    from pathlib import Path
    
    CAMPAIGN = "Miasma/Hades/TeamPCP"
    SUSPICIOUS_STRINGS = [
        "folderOpen",
        "setup.js",
        "SessionStart",
        "masscan.cloud",
        "getsession.org",
        "git-tanstack.com",
        "EveryBoiWeBuildIsAWormyBoi",
        "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner",
        "OhNoWhatsGoingOnWithGitHub",
        "Hades: The End for the Damned",
    ]
    
    def read_text(path: Path) -> str:
        try:
            return path.read_text(encoding="utf-8", errors="ignore")
        except (UnicodeDecodeError, OSError):
            return ""
    
    def audit_tasks_json(path: Path, text: str) -> dict[str, object] | None:
        # Look for runOn: folderOpen
        has_folder_open = False
        has_setup_js = "setup.js" in text
        
        try:
            data = json.loads(text)
            tasks = data.get("tasks", [])
            if not isinstance(tasks, list):
                tasks = [tasks]
            for task in tasks:
                if not isinstance(task, dict):
                    continue
                run_options = task.get("runOptions", {})
                if isinstance(run_options, dict):
                    run_on = run_options.get("runOn")
                    if run_on == "folderOpen" or (isinstance(run_on, list) and "folderOpen" in run_on):
                        has_folder_open = True
        except Exception:
            # Fallback to regex/string match if JSON parsing fails (e.g., has comments)
            if "folderOpen" in text:
                has_folder_open = True
    
        if has_folder_open or has_setup_js:
            return {
                "file": str(path),
                "type": "tasks.json",
                "has_runOn_folderOpen": has_folder_open,
                "has_setup_js_ref": has_setup_js,
                "severity": "critical" if (has_folder_open and has_setup_js) else "medium",
                "reason": "VS Code task configured to run automatically on folder open or references setup.js.",
            }
        return None
    
    def audit_ai_settings_json(path: Path, text: str, tool_name: str) -> dict[str, object] | None:
        suspicious_hits = [s for s in SUSPICIOUS_STRINGS if s in text]
        
        if suspicious_hits or "setup.js" in text or "run" in text or "command" in text:
            return {
                "file": str(path),
                "type": f"{tool_name}_settings",
                "matched_indicators": suspicious_hits,
                "severity": "critical" if "setup.js" in text else "high",
                "reason": f"AI coding tool settings file for {tool_name} contains execution hooks or suspicious triggers.",
            }
        return None
    
    def audit_cursor_rule(path: Path, text: str) -> dict[str, object] | None:
        suspicious_hits = [s for s in SUSPICIOUS_STRINGS if s in text]
        
        if "setup.js" in text or "execute" in text.lower() or "run" in text.lower() or suspicious_hits:
            return {
                "file": str(path),
                "type": "cursor_rule",
                "matched_indicators": suspicious_hits,
                "severity": "high",
                "reason": "Cursor rule instructing agent to run setup scripts or contains Miasma indicators.",
            }
        return None
    
    def audit_setup_js(path: Path, text: str) -> dict[str, object] | None:
        suspicious_hits = [s for s in SUSPICIOUS_STRINGS if s in text]
        size_mb = path.stat().st_size / (1024 * 1024)
        
        is_large = size_mb > 1.0
        if is_large or suspicious_hits or "process.env" in text or "eval" in text:
            return {
                "file": str(path),
                "type": "payload_candidate",
                "file_size_mb": round(size_mb, 2),
                "matched_indicators": suspicious_hits,
                "severity": "critical",
                "reason": "Malicious payload candidate matching Miasma signature or execution characteristics.",
            }
        return None
    
    def iter_files(root: Path):
        exclude_dirs = {".git", "node_modules", "vendor", "dist", "venv", ".venv"}
        for path in root.rglob("*"):
            if path.is_file() and not any(part in path.parts for part in exclude_dirs):
                yield path
    
    def main() -> int:
        parser = argparse.ArgumentParser(description=__doc__)
        parser.add_argument("paths", nargs="*", type=Path, default=[Path(".")], help="Directories or files to scan (defaults to current directory)")
        parser.add_argument("--out", type=Path, default=Path("miasma-findings.json"), help="Output JSON path")
        args = parser.parse_args()
    
        findings = []
        
        for input_path in args.paths:
            if not input_path.exists():
                print(f"[-] Path does not exist: {input_path}", file=sys.stderr)
                continue
                
            paths = iter_files(input_path) if input_path.is_dir() else [input_path]
            for path in paths:
                name = path.name.lower()
                text = read_text(path)
                
                result = None
                if name == "tasks.json":
                    result = audit_tasks_json(path, text)
                elif name == "settings.json":
                    if ".claude" in path.parts:
                        result = audit_ai_settings_json(path, text, "claude")
                    elif ".gemini" in path.parts:
                        result = audit_ai_settings_json(path, text, "gemini")
                elif path.suffix == ".mdc" and ".cursor" in path.parts:
                    result = audit_cursor_rule(path, text)
                elif name == "setup.js" or (path.suffix == ".js" and "setup" in name):
                    result = audit_setup_js(path, text)
                
                if result:
                    findings.append(result)
    
        payload = {
            "campaign": CAMPAIGN,
            "suspicious_strings": SUSPICIOUS_STRINGS,
            "finding_count": len(findings),
            "findings": findings,
        }
        
        args.out.write_text(json.dumps(payload, indent=2), encoding="utf-8")
        print(f"[+] Wrote {len(findings)} findings to {args.out}")
        
        if findings:
            print("\n[!] SUSPICIOUS OR MALICIOUS ARTIFACTS FOUND:")
            for f in findings:
                print(f"  - [{f['severity'].upper()}] {f['file']}: {f['reason']}")
                
        return 2 if findings else 0
    
    if __name__ == "__main__":
        raise SystemExit(main())

    Remediation and Recovery

    Containment

    1. Isolate Affected Workstations: Immediately disconnect developer machines from local networks and the internet if a compromised repository was opened with auto-run enabled.
    2. Disable Auto-Run in VS Code: Set "git.autoRepositoryTrust": false and "task.allowAutomaticTasks": "off" in global user settings.
    3. Remove files: Delete the cloned repository folder from local disks.

    Eradication

    1. Rotate Credentials: Revoke and rotate all secrets on the affected machine, including:
      • AWS Access Key IDs and Secret Access Keys.
      • Azure Service Principal client secrets.
      • GCP service account keys.
      • GitHub Personal Access Tokens (PATs) and SSH keys.
      • Kubernetes kubeconfig client certificates.
    2. Clean Python Caches: Check for any rogue .pth files in Python site-packages directories (e.g. *-setup.pth).

    Recovery

    1. Deploy Clean Repositories: Re-clone repositories after verifying they have been restored by administrators.
    2. Implement MFA & OIDC: Enforce multi-factor authentication and OpenID Connect (OIDC) trusted publishing workflows for all internal deployments and code repositories.

    Sources

    1. ThreatLocker: The Spreading Blight - Miasma Supply Chain Campaign targeting AI Tools
    2. StepSecurity: Miasma Hades Compromise in Azure DurableTask
    3. Phoenix Security: Hades Wave - Supply Chain Campaign Explores AI Coding Assistants
    4. Zscaler ThreatLabz: Miasma Hades Campaign targeting Developer IDEs and AI Assistants

    IOC Clipboard

    17 IOCs
    Defang IOCs
    domain api.masscan.cloud api[.]masscan[.]cloud
    domain filev2.getsession.org filev2[.]getsession[.]org
    domain seed1.getsession.org seed1[.]getsession[.]org
    domain seed2.getsession.org seed2[.]getsession[.]org
    domain seed3.getsession.org seed3[.]getsession[.]org
    domain git-tanstack.com git-tanstack[.]com
    url https://api.masscan.cloud hxxps://api[.]masscan[.]cloud
    url https://filev2.getsession.org hxxps://filev2[.]getsession[.]org
    url https://seed1.getsession.org hxxps://seed1[.]getsession[.]org
    url https://seed2.getsession.org hxxps://seed2[.]getsession[.]org
    url https://seed3.getsession.org hxxps://seed3[.]getsession[.]org
    url https://git-tanstack.com hxxps://git-tanstack[.]com
    file .vscode/tasks.json .vscode/tasks.json
    file .claude/settings.json .claude/settings.json
    file .gemini/settings.json .gemini/settings.json
    file .cursor/rules/setup.mdc .cursor/rules/setup.mdc
    file .github/setup.js .github/setup.js