critical Threat analysis

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.

#pypi#startup-hook#supply-chain#credential-theft#hades-cluster
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

    ClaimStatusEvidence
    Socket researchers disclosed a new PyPI supply-chain campaign on 2026-06-07.confirmedSocket’s blog post describes the Hades campaign, listing the compromised PyPI packages [Source 1].
    The campaign uses Python startup hooks to execute automatically.confirmedThe 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.confirmedStolen 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.confirmedNetwork logs from analyzed sandboxes show decoy queries designed to mask egress channels [Source 1].

    Impact Determination

    ClassificationCriteriaRequired evidenceHandling decisionClosure condition
    Confirmed compromiseAffected 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 exposedCompromised 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 exposedDependency 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 exposedNo 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.
    UnknownPackage 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

    1. Socket: Shai-Hulud Descends to Hades - Role: PRIMARY_RESEARCH - Impact: Campaign details, startup hooks mechanism, compromised packages.
    2. Security Online: Miasma Worm Spreads with New PyPI Wave - Role: SECONDARY_ANALYSIS - Impact: Broader industry reporting and correlation.
    3. PyPI: Python Package Index - Role: DIRECT_SOURCE - Impact: Package repository metadata.

    IOC Clipboard

    2 IOCs
    Defang IOCs
    file hades-setup.pth hades-setup.pth
    file _index.js _index.js