critical Threat analysis

Microsoft DurableTask Python SDK PyPI Hijacking

On May 19, 2026, the official Microsoft durabletask Python SDK was compromised on PyPI. Threat actors used hijacked publishing credentials to directly upload malicious versions containing a cloud credential-harvesting payload.

#pypi#package-compromise#supply-chain#credential-theft#microsoft#teampcp
On this page 0% read

    Executive Summary

    On May 19, 2026, the official Microsoft Python SDK durabletask (widely used for building stateful orchestrations in serverless and distributed environments) was compromised in a severe software supply chain attack StepSecurity Incident Registry. Attackers hijacked the PyPI publishing credentials (likely via a leaked API token or account takeover) and bypassed Microsoft’s source repository and build pipeline entirely Snyk Security Blog. They directly uploaded three compromised versions to PyPI: 1.4.1, 1.4.2, and 1.4.3 StepSecurity Incident Registry. The malicious packages contained a dropper payload designed to download and execute rope.pyz—a highly sophisticated, multi-stage credential harvesting and exfiltration framework attributed to the cybercrime group TeamPCP JFrog Security Research. The payload scraped developer workspaces, CI/CD runners, and active environment memories to steal AWS, Google Cloud, Azure, and Kubernetes secrets, exfiltrating them to TeamPCP-controlled C2 servers. CISA and Microsoft security teams intervened to yank the compromised releases and revoke the compromised token. Purge affected caches, then use the lockfile, process, and downstream audit recipes below to determine whether rope.pyz executed and which identities were reachable.

    Key Facts

    threat_type: "Registry-Only Malicious Package Upload & Credential Theft"
    ecosystem: "pypi, python"
    registry: "PyPI Registry"
    affected_packages:
      - "durabletask"
    malicious_versions:
      - "1.4.1"
      - "1.4.2"
      - "1.4.3"
    fixed_versions:
      - "1.4.4"
    safe_versions:
      - "1.4.0"
      - "1.4.4"
    exposure_window: "2026-05-19T06:00:00Z to 2026-05-19T17:30:00Z"
    execution_trigger: "Installing the package or executing workflows pulling versions 1.4.1 - 1.4.3 during runtime or testing"
    primary_impact: "Host and runner memory scraping, secret harvesting, and automated C2 exfiltration"
    known_iocs:
      - "rope[.]pyz"
      - "filev2.getsession[.]org"
      - "api.masscan[.]cloud"
    confidence: "high"
    canonical_source: "https://www.stepsecurity.io"

    Source Confidence & Evidence Mapping

    • confirmed:
      • Malicious versions of durabletask published on PyPI were 1.4.1, 1.4.2, and 1.4.3. Source: StepSecurity Incident Registry
      • The attack bypassed Microsoft’s repository build pipelines and was uploaded using compromised registry publishing credentials. Source: Snyk Security Blog
      • The injected package acted as a dropper for the rope.pyz malicious framework. Source: JFrog Security Research
    • likely:
    • unclear:
      • Whether the credentials were stolen via developer workstation compromise or leaked through a public GitHub Action log. Source: JFrog Security Research

    Impact Determination

    ClassificationCriteriaRequired evidenceRequired actionClosure condition
    Confirmed compromisedurabletask==1.4.1, 1.4.2, or 1.4.3 is present and setup/install-time dropper executes rope.pyz or the reported process, file, or network indicators is observed.Artifact inventory plus runtime telemetry showing setup/install-time dropper executes rope.pyz or listed C2/process/file indicators.Isolate affected hosts or runners, preserve artifacts, and rotate reachable credentials from a clean environment.Affected artifacts are removed, exposed credentials are replaced, and downstream audit modules show no suspicious follow-on use.
    Presumed exposeddurabletask==1.4.1, 1.4.2, or 1.4.3 was installed, pulled, imported, built, or executed during the exposure window, but telemetry cannot prove exfiltration.Lockfile, package cache, workflow, image pull, extension inventory, build log, or deployment record tied to the exposure window.Rebuild from clean artifacts and rotate credentials available to the affected environment.Credential owners confirm revocation of old material and clean artifacts are deployed.
    Potentially exposedThe package, workflow, image, extension, or module appears in dependency or deployment records, but Python install, import, or interpreter-startup execution is not established.Manifest, lockfile, build, deployment, or endpoint records plus a named telemetry gap.Collect the missing execution and telemetry evidence before narrowing scope.Every hit is dispositioned as confirmed compromise, presumed exposed, or not exposed.
    Not exposedNo affected version, artifact, mutable reference, or indicator appears in source, lockfiles, build outputs, deployments, package caches, or runtime telemetry.Repository search, dependency inventory, build/deployment export, package cache query, and runtime telemetry query results.Preserve the negative search output and keep the prevention controls active.Search evidence covers developer endpoints, CI runners, production deployments, and package or image caches.
    UnknownRequired inventory, build, endpoint, network, or audit telemetry is unavailable.A gap statement naming unavailable systems, owners, and time windows.Keep the asset in scope and make conservative rotation or rebuild decisions for high-value environments.The missing evidence is recovered or the risk owner accepts residual uncertainty.

    Minimum Evidence To Collect

    minimum_evidence:
      - "Dependency, workflow, extension, image, or module inventory covering developer endpoints, CI runners, and production deployments."
      - "Positive or negative search results for durabletask==1.4.1, durabletask==1.4.2, durabletask==1.4.3."
      - "Execution evidence for setup/install-time dropper executes `rope.pyz`."
      - "Process, file, DNS, proxy, firewall, or package-manager telemetry for listed indicators."
      - "Inventory of credentials, tokens, deployment paths, and downstream systems reachable from exposed environments."

    Timeline

    • 2026-05-19T06:00:00Z Attackers exploit a leaked PyPI token associated with the Microsoft package, uploading 1.4.1, 1.4.2, and 1.4.3 directly to PyPI. Source: StepSecurity Incident Registry
    • 2026-05-19T08:30:00Z Automated threat intelligence systems at StepSecurity detect abnormal library size expansion and anomalous package structural signatures. Source: StepSecurity Incident Registry
    • 2026-05-19T10:15:00Z Snyk and Microsoft Security teams begin analysis of the dropped file rope.pyz. Source: Snyk Security Blog
    • 2026-05-19T17:30:00Z PyPI administrators remove the malicious releases and invalidate the compromised publishing tokens. Source: StepSecurity Incident Registry

    What Happened

    On May 19, 2026, enterprise security teams running automated dependency scanners flagged an unexpected patch release for Microsoft’s durabletask library on PyPI StepSecurity Incident Registry. Inspection of the underlying PyPI metadata revealed that the releases were uploaded via a legacy API token rather than the standard OpenID Connect (OIDC) Trusted Publishing workflow that Microsoft normally enforces for its SDK builds Snyk Security Blog. Inside the package archives, analysts discovered a modified setup file that executed dynamically on installation, dropping an executable archive named rope.pyz JFrog Security Research. The dropper bypassed Microsoft’s official GitHub repository, leaving the source code completely clean but leaving anyone who pulled the latest version from PyPI vulnerable StepSecurity Incident Registry. PyPI administrators quickly deleted the compromised releases and revoked all active publisher tokens for the package StepSecurity Incident Registry.

    Technical Analysis

    Initial Access

    Initial access was achieved using compromised registry publishing credentials Snyk Security Blog. Threat actors either obtained a leaked PyPI API token from an exposed workstation or leveraged an active credential harvested during earlier stages of their campaign against other projects StepSecurity Incident Registry.

    Package or Artifact Manipulation

    The repository microsoft/durabletask-python remained completely unaffected. The attackers downloaded the official 1.4.0 package, injected the malicious dropper into setup.py and the main module bundle, changed the version metadata to 1.4.1, 1.4.2, and 1.4.3, and uploaded the backdoored wheel and source distribution files directly to PyPI StepSecurity Incident Registry.

    Execution Trigger

    The malicious script was triggered automatically at install-time Snyk Security Blog. Because setup.py was altered, any system running:

    pip install durabletask

    or loading the dependency during standard CI/CD workflow provisioning automatically executed the dropper script JFrog Security Research.

    Payload Behavior

    Once triggered, the payload downloaded rope.pyz—an obfuscated Python zip application JFrog Security Research. The script unpacked the framework into the runner’s local execution environment, performing memory-scraping operations to harvest active credentials StepSecurity Incident Registry. The malware targeted AWS credentials, Azure tokens, Google Cloud secrets, and local environment variables, matching the signature credential-stealing mechanics of TeamPCP Snyk Security Blog.

    Exfiltration / C2

    Exfiltrated data was packaged and shipped via secure outbound web requests to TeamPCP-controlled C2 servers:

    • filev2.getsession[.]org
    • api.masscan[.]cloud

    These servers were used to store collected secret dumps and coordinate further automated package hijacking tasks StepSecurity Incident Registry.

    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 StepSecurity Incident Registry.

    Obfuscation or Evasion

    The rope.pyz payload utilized zip-application bundling to package multiple obfuscated Python files together, preventing simple directory-based file scanners from flagging individual raw malicious scripts on disk JFrog Security Research.

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "pypi"
      packages:
        - "durabletask"
      versions:
        - "1.4.1"
        - "1.4.2"
        - "1.4.3"
      repositories:
        - "microsoft/durabletask-python"
      container_images: []
      CI_CD_systems:
        - "GitHub Actions pipelines"
        - "Azure DevOps pipelines"
      developer_tools:
        - "Developer workstations"
    credentials_at_risk:
      - AWS access keys
      - Azure service principal tokens
      - Google Cloud credentials
      - PyPI publishing tokens

    Indicators of Compromise

    Domains

    • filev2.getsession[.]org (source: https://www.stepsecurity.io, confidence: high)
    • api.masscan[.]cloud (source: https://www.stepsecurity.io, confidence: high)

    File HasHas/Identifiers

    • rope.pyz (Malicious python execution framework)

    Package Versions

    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-microsoft-durabletask-pypi-compromise-scope"))
    SINCE = "2026-05-19T06:00:00Z"
    UNTIL = "2026-05-19T23:59:59Z"
    
    PACKAGES = [
      "durabletask",
    ]
    VERSIONS = [
      "1.4.1",
      "1.4.2",
      "1.4.3",
    ]
    FILES = [
    ]
    DOMAINS = [
      "www.stepsecurity.io",
    ]
    URLS = [
      "https://www.stepsecurity.io`",
    ]
    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)
            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-05-19T06:00:00Z"
    UNTIL = "2026-05-19T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-microsoft-durabletask-pypi-compromise-github-audit"))
    
    SELECTORS = [
      "durabletask",
      "1.4.1",
      "1.4.2",
      "1.4.3",
      "www.stepsecurity.io",
      "https://www.stepsecurity.io`",
    ]
    
    # 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-05-19T06:00:00Z"
    UNTIL = "2026-05-19T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-microsoft-durabletask-pypi-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-05-19T06:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-microsoft-durabletask-pypi-compromise-registry-audit"))
    PACKAGES = [
      "durabletask",
    ]
    VERSIONS = [
      "1.4.1",
      "1.4.2",
      "1.4.3",
    ]
    
    # 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. StepSecurity DurableTask Analysis - Role: DIRECT_SOURCE - Impact: Detailed version numbers, timeline timestamps, and OIDC bypass analysis.
    2. Snyk Security Blog on PyPI Threat Vectors - Role: PRIMARY_RESEARCH - Impact: Explanation of token-hijacking and C2 infrastructure mapping.
    3. JFrog rope.pyz Technical Analysis - Role: PRIMARY_RESEARCH - Impact: Zip-app payload bundling mechanics and credential-scraping behavior details.

    IOC Clipboard

    2 IOCs
    Defang IOCs
    domain www.stepsecurity.io www[.]stepsecurity[.]io
    url https://www.stepsecurity.io` hxxps://www[.]stepsecurity[.]io`