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.
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
| Claim | Status | Evidence |
|---|---|---|
| The Azure/durabletask repository was compromised on June 5, 2026. | confirmed | ThreatLocker 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. | confirmed | StepSecurity 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). | confirmed | Phoenix 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. | confirmed | ThreatLocker 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. | confirmed | StepSecurity 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
| Classification | Criteria | Required evidence | Handling decision | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | An 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 exposed | An 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 exposed | The 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 exposed | The 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. |
| Unknown | Inventory, 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/durabletaskrepository. 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.jsonautomatically runs the configured task (executingnode .github/setup.js) as soon as the workspace folder is opened by a user. - Claude Code & Gemini CLI: The
SessionStarthook in.claude/settings.jsonand.gemini/settings.jsonexecutes the script when the AI session begins. - Cursor: The
.cursor/rules/setup.mdcfile uses prompt injection to instruct the Cursor AI agent to execute.github/setup.jsunder 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.cloudfilev2.getsession.orgseed1.getsession.orgseed2.getsession.orgseed3.getsession.orggit-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
- Isolate Affected Workstations: Immediately disconnect developer machines from local networks and the internet if a compromised repository was opened with auto-run enabled.
- Disable Auto-Run in VS Code: Set
"git.autoRepositoryTrust": falseand"task.allowAutomaticTasks": "off"in global user settings. - Remove files: Delete the cloned repository folder from local disks.
Eradication
- 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
kubeconfigclient certificates.
- Clean Python Caches: Check for any rogue
.pthfiles in Python site-packages directories (e.g.*-setup.pth).
Recovery
- Deploy Clean Repositories: Re-clone repositories after verifying they have been restored by administrators.
- Implement MFA & OIDC: Enforce multi-factor authentication and OpenID Connect (OIDC) trusted publishing workflows for all internal deployments and code repositories.
Sources
- ThreatLocker: The Spreading Blight - Miasma Supply Chain Campaign targeting AI Tools
- StepSecurity: Miasma Hades Compromise in Azure DurableTask
- Phoenix Security: Hades Wave - Supply Chain Campaign Explores AI Coding Assistants
- Zscaler ThreatLabz: Miasma Hades Campaign targeting Developer IDEs and AI Assistants
IOC Clipboard
17 IOCsapi.masscan.cloud api[.]masscan[.]cloud filev2.getsession.org filev2[.]getsession[.]org seed1.getsession.org seed1[.]getsession[.]org seed2.getsession.org seed2[.]getsession[.]org seed3.getsession.org seed3[.]getsession[.]org git-tanstack.com git-tanstack[.]com https://api.masscan.cloud hxxps://api[.]masscan[.]cloud https://filev2.getsession.org hxxps://filev2[.]getsession[.]org https://seed1.getsession.org hxxps://seed1[.]getsession[.]org https://seed2.getsession.org hxxps://seed2[.]getsession[.]org https://seed3.getsession.org hxxps://seed3[.]getsession[.]org https://git-tanstack.com hxxps://git-tanstack[.]com .vscode/tasks.json .vscode/tasks.json .claude/settings.json .claude/settings.json .gemini/settings.json .gemini/settings.json .cursor/rules/setup.mdc .cursor/rules/setup.mdc .github/setup.js .github/setup.js