Hades Cluster PyPI Worm Abuses Python Startup Hooks
Socket researchers disclosed a June 7, 2026 PyPI supply-chain campaign where attackers compromised 19 legitimate scientific research and deep-learning packages. The malware abuses Python startup hooks (*-setup.pth) to execute automatically, bootstrap Bun, and steal credentials.
On this page 0% read
Executive Summary
On 2026-06-07, Socket’s research team disclosed a coordinated PyPI supply-chain compromise campaign dubbed the Hades Cluster [Source 1]. Attackers gained publishing authority on 19 legitimate scientific research, bioinformatics, and deep-learning packages, uploading a total of 37 malicious wheels [Source 1].
The malware exploits Python’s startup behavior by dropping a hidden configuration file (*-setup.pth) inside the package directories [Source 1] [Source 2]. When the Python interpreter starts in an environment containing the compromised packages, this file executes automatically—even if the developer does not explicitly import the package [Source 1] [Source 2]. The startup hook downloads the Bun runtime and launches a credential stealer targeting developer secrets, AWS/GCP/Azure cloud tokens, and GitHub/npm credentials [Source 1]. Treat any install or execution of these packages as a high-risk compromise.
Source-Watcher Candidate Queue
candidate_id: "hades-cluster-pypi-startup-hook-compromise"
first_seen: "2026-06-07"
decision: "publish_ready"
relationship: "candidate_child_event_of_mini_shai_hulud_miasma"
dedupe_keys:
- "technique:pth-startup-hook"
- "tool:bun"
- "campaign:hades-cluster"
- "pypi:bramin"
- "pypi:okite"
starting_sources:
- "Socket primary research"
- "Security Online intelligence page"
- "PyPI package registry metadata"
Key Facts
threat_type: "malicious PyPI package startup-time execution"
ecosystem: "pypi, python"
technique: "Python *.pth startup hook abuse"
campaign_name: "Hades Cluster"
related_family: "Miasma / Mini Shai-Hulud"
disclosed: "2026-06-07"
execution_trigger:
- "Python startup execution via *.pth files"
- "Any Python execution in environment containing compromised packages"
known_affected_packages:
- "bramin"
- "cmd2func"
- "coolbox"
- "dynamo-release"
- "executor-engine"
- "executor-http"
- "funcdesc"
- "magique"
- "magique-ai"
- "mrbios"
- "napari-ufish"
- "nucbox"
- "okite"
- "pantheon-agents"
- "pantheon-toolsets"
- "spateo-release"
- "synago"
- "ufish"
- "uprobe"
credential_risk:
- "pypi tokens"
- "npm tokens"
- "GitHub tokens"
- "cloud credentials"
- "SSH keys"
- "CI/CD secrets"
Source Confidence and Claim Ledger
| Claim | Status | Evidence |
|---|---|---|
| Socket researchers disclosed a new PyPI supply-chain campaign on 2026-06-07. | confirmed | Socket’s blog post describes the Hades campaign, listing the compromised PyPI packages [Source 1]. |
| The campaign uses Python startup hooks to execute automatically. | confirmed | The malware drops a *-setup.pth file which triggers code execution when the Python interpreter initializes [Source 1] [Source 2]. |
| Stolen credentials are exfiltrated to GitHub repositories with Hades-themed descriptors. | confirmed | Stolen data is packaged and sent to newly created GitHub repositories described with the phrase “Hades - The End for the Damned” [Source 1]. |
| The malware emits decoy traffic to Anthropic AI servers. | confirmed | Network logs from analyzed sandboxes show decoy queries designed to mask egress channels [Source 1]. |
Impact Determination
| Classification | Criteria | Required evidence | Handling decision | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | Affected PyPI package version was installed and startup execution or C2 exfiltration is observed. | Telemetry proving execution of _index.js, Bun runtime download, or stygian/cerberus repository creation. | Isolate affected host/runner immediately and rotate all reachable cloud and VCS tokens. | Removal of the package, cleanup of the Python environment, and verification of downstream access logs. |
| Presumed exposed | Compromised package is found in project lockfiles, requirements, or local cache directories. | requirements.txt, poetry.lock, pipfile, or local site-packages inspection. | Assume local credentials available to that runtime are compromised; proceed with rotation. | Package replacement with clean versions and completion of credential rotation. |
| Potentially exposed | Dependency matches names of affected bioinformatics/deep-learning packages, but version is clean. | Manifest check showing clean versions installed. | Verify that no cached wheels or local modifications were pulled. | Version verification shows clean tags. |
| Not exposed | No affected package names or indicators found in environment or network log. | Registry logs, package caches, and system process history search. | Document negative result and monitor for registry-level changes. | Environment and registry verification. |
| Unknown | Package manager logs or system history are missing. | Gap in local logging or endpoint agent telemetry. | Retain standard scoping and execute proactive rotation on high-value tokens. | Restoration of logs or fallback to presumptive handling. |
Timeline
- 2026-06-07: Socket publishes primary research detailing the Hades cluster PyPI campaign [Source 1].
- 2026-06-07: The Halting Problems refresher identifies the campaign as a new unreported candidate and publishes this analysis.
Machine-Readable Event Profile
{
"event_id": "hades-cluster-pypi-startup-hook-compromise",
"title": "Hades Cluster PyPI Worm Abuses Python Startup Hooks",
"first_seen": "2026-06-07",
"published": "2026-06-07",
"severity": "critical",
"ecosystem": ["pypi", "python", "GitHub Actions"],
"campaign_context": "Hades Cluster / Miasma / Mini Shai-Hulud",
"affected_packages": [
"bramin",
"cmd2func",
"coolbox",
"dynamo-release",
"executor-engine",
"executor-http",
"funcdesc",
"magique",
"magique-ai",
"mrbios",
"napari-ufish",
"nucbox",
"okite",
"pantheon-agents",
"pantheon-toolsets",
"spateo-release",
"synago",
"ufish",
"uprobe"
],
"known_malicious_versions": {
"bramin": ["0.0.2", "0.0.3", "0.0.4"],
"cmd2func": ["0.2.2", "0.2.3"],
"coolbox": ["0.4.1", "0.4.2"],
"dynamo-release": ["1.5.4"],
"executor-engine": ["0.3.4", "0.3.5"],
"executor-http": ["0.1.3", "0.1.4"],
"funcdesc": ["0.2.2", "0.2.3"],
"magique": ["0.6.8", "0.6.9"],
"magique-ai": ["0.4.4", "0.4.5"],
"mrbios": ["0.1.1", "0.1.2"],
"napari-ufish": ["0.0.2", "0.0.3"],
"nucbox": ["0.1.2", "0.1.3"],
"okite": ["0.0.7", "0.0.8"],
"pantheon-agents": ["0.6.1", "0.6.2"],
"pantheon-toolsets": ["0.5.5", "0.5.6"],
"spateo-release": ["1.1.2"],
"synago": ["0.1.1", "0.1.2"],
"ufish": ["0.1.2", "0.1.3"],
"uprobe": ["0.1.3", "0.1.4"]
},
"known_behaviors": [
"automatic execution on Python startup via *.pth files",
"download and boot of Bun runtime for credential theft",
"automated repository creation with Hades themed metadata for exfiltration",
"decoy egress to Anthropic AI servers"
],
"primary_sources": [
"https://socket.dev/blog/shai-hulud-descends-to-hades-miasma-worm-campaign-spreads-with-new-pypi-wave",
"https://securityonline.info/shai-hulud-descends-to-hades-miasma-worm-campaign-spreads-with-new-pypi-wave/",
"https://pypi.org/"
]
}
Indicators of Compromise
package_versions:
- "bramin==0.0.2"
- "bramin==0.0.3"
- "bramin==0.0.4"
- "cmd2func==0.2.2"
- "cmd2func==0.2.3"
- "coolbox==0.4.1"
- "coolbox==0.4.2"
- "dynamo-release==1.5.4"
- "executor-engine==0.3.4"
- "executor-engine==0.3.5"
- "executor-http==0.1.3"
- "executor-http==0.1.4"
- "funcdesc==0.2.2"
- "funcdesc==0.2.3"
- "magique==0.6.8"
- "magique==0.6.9"
- "magique-ai==0.4.4"
- "magique-ai==0.4.5"
- "mrbios==0.1.1"
- "mrbios==0.1.2"
- "napari-ufish==0.0.2"
- "napari-ufish==0.0.3"
- "nucbox==0.1.2"
- "nucbox==0.1.3"
- "okite==0.0.7"
- "okite==0.0.8"
- "pantheon-agents==0.6.1"
- "pantheon-agents==0.6.2"
- "pantheon-toolsets==0.5.5"
- "pantheon-toolsets==0.5.6"
- "spateo-release==1.1.2"
- "synago==0.1.1"
- "synago==0.1.2"
- "ufish==0.1.2"
- "ufish==0.1.3"
- "uprobe==0.1.3"
- "uprobe==0.1.4"
files:
- "hades-setup.pth"
- "_index.js"
process_patterns:
- "Python startup executions via *.pth configuration files"
telemetry_selectors:
- "Hades - The End for the Damned"
- "hades-setup.pth"
- "tartarean"
- "cerberus"
- "charon"
- "thanatos"
- "stygian"
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-hades-cluster-pypi-startup-hook-compromise-scope"))
SINCE = "2026-06-07T00:00:00Z"
UNTIL = "2026-06-07T23:59:59Z"
PACKAGES = [
]
VERSIONS = [
"bramin==0.0.2",
"bramin==0.0.3",
"bramin==0.0.4",
"cmd2func==0.2.2",
"cmd2func==0.2.3",
"coolbox==0.4.1",
"coolbox==0.4.2",
"dynamo-release==1.5.4",
"executor-engine==0.3.4",
"executor-engine==0.3.5",
"executor-http==0.1.3",
"executor-http==0.1.4",
"funcdesc==0.2.2",
"funcdesc==0.2.3",
"magique==0.6.8",
"magique==0.6.9",
"magique-ai==0.4.4",
"magique-ai==0.4.5",
"mrbios==0.1.1",
"mrbios==0.1.2",
"napari-ufish==0.0.2",
"napari-ufish==0.0.3",
"nucbox==0.1.2",
"nucbox==0.1.3",
"okite==0.0.7",
"okite==0.0.8",
"pantheon-agents==0.6.1",
"pantheon-agents==0.6.2",
"pantheon-toolsets==0.5.5",
"pantheon-toolsets==0.5.6",
"spateo-release==1.1.2",
"synago==0.1.1",
"synago==0.1.2",
"ufish==0.1.2",
"ufish==0.1.3",
"uprobe==0.1.3",
"uprobe==0.1.4",
]
FILES = [
"hades-setup.pth",
"_index.js",
]
DOMAINS = [
]
URLS = [
]
IPS = [
]
HASHES = [
]
PROCESS_PATTERNS = [
"Python startup executions via *.pth configuration files",
]
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)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying pip index for {package}...")
res = subprocess.run(["python3", "-m", "pip", "index", "versions", package], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"pypi-{safe_name}-versions.txt").write_text(res.stdout)
subprocess.run(["python3", "-m", "pip", "download", "--no-deps", package, "-d", str(registry_dir)], capture_output=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-07T00:00:00Z"
UNTIL = "2026-06-07T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-hades-cluster-pypi-startup-hook-compromise-github-audit"))
SELECTORS = [
"bramin==0.0.2",
"bramin==0.0.3",
"bramin==0.0.4",
"cmd2func==0.2.2",
"cmd2func==0.2.3",
"coolbox==0.4.1",
"coolbox==0.4.2",
"dynamo-release==1.5.4",
"executor-engine==0.3.4",
"executor-engine==0.3.5",
"executor-http==0.1.3",
"executor-http==0.1.4",
"funcdesc==0.2.2",
"funcdesc==0.2.3",
"magique==0.6.8",
"magique==0.6.9",
"magique-ai==0.4.4",
"magique-ai==0.4.5",
"mrbios==0.1.1",
"mrbios==0.1.2",
"napari-ufish==0.0.2",
"napari-ufish==0.0.3",
"nucbox==0.1.2",
"nucbox==0.1.3",
"okite==0.0.7",
"okite==0.0.8",
"pantheon-agents==0.6.1",
"pantheon-agents==0.6.2",
"pantheon-toolsets==0.5.5",
"pantheon-toolsets==0.5.6",
"spateo-release==1.1.2",
"synago==0.1.1",
"synago==0.1.2",
"ufish==0.1.2",
"ufish==0.1.3",
"uprobe==0.1.3",
"uprobe==0.1.4",
"hades-setup.pth",
"_index.js",
]
# 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-07T00:00:00Z"
UNTIL = "2026-06-07T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-hades-cluster-pypi-startup-hook-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-07T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-hades-cluster-pypi-startup-hook-compromise-registry-audit"))
PACKAGES = [
]
VERSIONS = [
"bramin==0.0.2",
"bramin==0.0.3",
"bramin==0.0.4",
"cmd2func==0.2.2",
"cmd2func==0.2.3",
"coolbox==0.4.1",
"coolbox==0.4.2",
"dynamo-release==1.5.4",
"executor-engine==0.3.4",
"executor-engine==0.3.5",
"executor-http==0.1.3",
"executor-http==0.1.4",
"funcdesc==0.2.2",
"funcdesc==0.2.3",
"magique==0.6.8",
"magique==0.6.9",
"magique-ai==0.4.4",
"magique-ai==0.4.5",
"mrbios==0.1.1",
"mrbios==0.1.2",
"napari-ufish==0.0.2",
"napari-ufish==0.0.3",
"nucbox==0.1.2",
"nucbox==0.1.3",
"okite==0.0.7",
"okite==0.0.8",
"pantheon-agents==0.6.1",
"pantheon-agents==0.6.2",
"pantheon-toolsets==0.5.5",
"pantheon-toolsets==0.5.6",
"spateo-release==1.1.2",
"synago==0.1.1",
"synago==0.1.2",
"ufish==0.1.2",
"ufish==0.1.3",
"uprobe==0.1.3",
"uprobe==0.1.4",
]
# Positive signal: registry metadata, package tarballs, or cached artifacts contain the exact affected package/version values.
# Remediation trigger: any internal package cache, build artifact, or deployment using these package/version values requires exposure scoping.
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. Audit PyPI dependencies in project files
print("[+] Scanning PyPI dependency files...")
for file in ["requirements.txt", "poetry.lock", "Pipfile.lock", "pyproject.toml", "setup.py"]:
if Path(file).exists():
subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
# 2. Query registry metadata and download packages for local analysis
packages_dir = OUT / "packages"
metadata_dir = OUT / "metadata"
packages_dir.mkdir(exist_ok=True)
metadata_dir.mkdir(exist_ok=True)
for package in PACKAGES:
if not package: continue
print(f"[+] Querying pip index for {package}...")
res = subprocess.run(["python3", "-m", "pip", "index", "versions", package], capture_output=True, text=True)
if res.returncode == 0:
(metadata_dir / f"{package}-versions.txt").write_text(res.stdout)
subprocess.run(["python3", "-m", "pip", "download", "--no-deps", package, "-d", str(packages_dir)], capture_output=True)
# 3. HOW TO REVOKE AND ROTATE EXPOSED PYPI PUBLISHING TOKENS:
# PyPI does not support token revocation via CLI. Follow these exact steps:
# 1. Log in to https://pypi.org/manage/account/
# 2. Scroll to the "API tokens" section and click "Remove" on any compromised tokens.
# 3. Generate a new API token limited to the specific project scope.
# 4. Update your CI/CD secrets using the GitHub CLI:
# subprocess.run(["gh", "secret", "set", "PYPI_API_TOKEN", "--body", "pypi-AgEIcHlwaS5vcm...", "--repo", "my-org/my-repo"])
print(f"[+] Wrote registry audit artifacts under {OUT}")
Sources
- Socket: Shai-Hulud Descends to Hades - Role: PRIMARY_RESEARCH - Impact: Campaign details, startup hooks mechanism, compromised packages.
- Security Online: Miasma Worm Spreads with New PyPI Wave - Role: SECONDARY_ANALYSIS - Impact: Broader industry reporting and correlation.
- PyPI: Python Package Index - Role: DIRECT_SOURCE - Impact: Package repository metadata.
IOC Clipboard
2 IOCshades-setup.pth hades-setup.pth _index.js _index.js