high Threat analysis

semantic-types PyPI Solana Keypair Monkey Patch

Socket reported that semantic-types became malicious at version 0.1.5 and 0.1.6, with five Solana-themed PyPI packages pulling it transitively. The payload monkey-patched solders.keypair.Keypair constructors, encrypted Solana private keys with an RSA-2048 public key, and exfiltrated ciphertext through Solana Devnet SPL memo transactions.

#pypi#supply-chain#solana#cryptocurrency#monkey-patching
On this page 0% read

    Executive Summary

    Socket reported a PyPI campaign by the alias cappership in which semantic-types carried the malicious payload and five Solana-themed packages pulled it transitively: solana-keypair, solana-publickey, solana-mev-agent-py, solana-trading-bot, and soltrade [Source 1].

    The malicious semantic-types update landed at 0.1.5 on January 26, 2025; 0.1.6 repackaged the same payload on January 28, 2025 [Source 1]. The payload monkey-patched solders.keypair.Keypair.from_seed, from_bytes, and from_base58_string, encrypted captured private key bytes with a hardcoded RSA-2048 public key, and sent the ciphertext as an SPL memo transaction through Solana Devnet RPC at api.devnet.solana.com [Source 1].

    There is no conventional attacker C2 domain to block for the key theft path. The hard indicators are package names/versions, PyPI publisher metadata from the Socket report, the Solana Devnet RPC endpoint, the SPL Memo program ID, the actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko, the RSA public-key fingerprint 5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62, and code paths that generate Solana keypairs while the malicious package is present [Source 1].

    Key Facts

    event_type: "malicious PyPI package with transitive dependency delivery"
    ecosystem: "PyPI"
    publisher:
      alias: "cappership"
      email: "[email protected]"
    malicious_payload_package:
      name: "semantic-types"
      malicious_versions:
        - "0.1.5"
        - "0.1.6"
    transitive_carrier_packages:
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    execution_trigger: "Python import that registers monkey patches, followed by solders Keypair constructor use"
    patched_methods:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"
    collection_window_utc:
      start: "2025-01-26T00:00:00Z"
      end: "2025-05-29T23:59:59Z"
    network_iocs:
      - "api.devnet.solana.com"
    solana_iocs:
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto_iocs:
      rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
    credentials_at_risk:
      - "Solana private keys created or imported through patched solders Keypair constructors"

    Source Confidence & Evidence Mapping

    • confirmed: Socket identified semantic-types as the core malicious package and solana-keypair, solana-publickey, solana-mev-agent-py, solana-trading-bot, and soltrade as dependent carrier packages [Source 1].
    • confirmed: Socket states semantic-types 0.1.5 introduced the malicious payload on January 26, 2025 and 0.1.6 repackaged it on January 28, 2025 [Source 1].
    • confirmed: Socket identified the monkey-patched methods Keypair.from_seed, Keypair.from_bytes, and Keypair.from_base58_string, the Solana Devnet RPC endpoint, the threat actor public key, the PyPI alias/email, and RSA public-key fingerprint [Source 1].
    • unclear: Public sources do not name victim wallets or prove which Devnet memo transactions correspond to real production keypairs.
    • not_observed: No evidence indicates compromise of the legitimate solders package itself.

    Impact Determination

    ClassificationCriteriaEvidence to collectHandling decision
    Confirmed compromiseA malicious package was installed and a patched solders.keypair.Keypair constructor generated or imported a real Solana keypair.Installed package/version evidence, code path invoking Keypair.from_seed, Keypair.from_bytes, or Keypair.from_base58_string, Solana Devnet RPC sendTransaction, memo program use, actor public key D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko.Treat keypairs created or imported through the process as exposed; move funds and authority to keys generated in a clean environment.
    Presumed exposedsemantic-types==0.1.5 or 0.1.6, or any carrier package, was installed in an environment that runs Solana key generation/import code, but runtime telemetry is missing.pip freeze, lockfile, package cache, source grep for patched methods, notebook history, CI output.Inventory all keypairs generated or imported by that environment after January 26, 2025 and replace them from clean tooling.
    Potentially exposedA carrier package appears in manifests or lockfiles, but installation or malicious dependency resolution is incomplete.Dependency graph, package proxy records, lockfile history, virtualenv/container inventory.Collect package-cache and environment evidence until the asset is classified.
    Not exposedNo malicious package, carrier package, affected version, Solana keypair constructor, Devnet RPC, memo program, or actor key appears in source, environments, caches, or telemetry.Negative repository search, package inventory, dependency lock, virtualenv/container search, and network telemetry search.Keep negative evidence with the case record and close this event for the asset.
    UnknownPackage inventory, dependency resolution history, endpoint data, source history, or Solana RPC telemetry is unavailable.Named telemetry gap with system, owner, and retention status.Keep Solana key material in scope until the missing evidence is recovered or key replacement is accepted as the closure path.

    Minimum Evidence To Collect

    package_evidence:
      - "semantic-types==0.1.5"
      - "semantic-types==0.1.6"
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    code_evidence:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"
    network_evidence:
      - "api.devnet.solana.com"
      - "sendTransaction"
      - "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      - "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto_evidence:
      - "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"

    Timeline

    • 2024-12-22: Socket reports benign semantic-types 0.1.2 and solana-trading-bot 0.1.0 were published [Source 1].
    • 2024-12-23: Socket reports updates for semantic-types 0.1.4, solana-trading-bot 0.1.1, and soltrade 0.1.1 with dependencies still benign [Source 1].
    • 2025-01-26: Socket reports semantic-types 0.1.5 introduced the malicious monkey-patching payload; solana-mev-agent-py 0.1.0 and solana-keypair 0.1.0 were also published [Source 1].
    • 2025-01-28: Socket reports semantic-types 0.1.6 repackaged the same malicious payload [Source 1].
    • 2025-02-04: Socket reports solana-keypair 0.2.1 and solana-publickey 0.2.1 were released and imported semantic-types [Source 1].
    • 2025-05-29: Socket published the public analysis with IOCs and the package cluster [Source 1].

    What Happened

    The actor used dependency relationships rather than a one-package-only lure. semantic-types contained the payload, while five Solana-themed packages depended on it and served as delivery paths [Source 1].

    Once imported, the payload modified methods on solders.keypair.Keypair at runtime. Calls to from_seed, from_bytes, and from_base58_string still returned a usable keypair, but the wrapper also sent key material to the attacker’s collection path through a background thread [Source 1].

    The exfiltration path used normal Solana Devnet RPC. Captured private key bytes were encrypted with the actor’s RSA public key, base64 encoded, embedded in an SPL Memo transaction, and broadcast to Devnet. The attacker could later read memo transactions associated with the actor public key and decrypt ciphertext offline [Source 1].

    Technical Analysis

    Package Manipulation

    payload_package: "semantic-types"
    malicious_versions:
      - "0.1.5"
      - "0.1.6"
    carrier_packages:
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    dependency_trigger: "pip resolves semantic-types from carrier package dependencies"

    Runtime Hook

    The hook targets the solders keypair class object already used by Solana Python developers. Monkey patching means source code that imports solders can look normal while runtime method dispatch has changed inside the interpreter [Source 1].

    Exfiltration

    transport:
      protocol: "Solana JSON-RPC over HTTPS"
      endpoint: "https://api.devnet.solana.com"
      rpc_method: "sendTransaction"
      program: "SPL Memo"
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    payload_encoding:
      key_material: "64-byte private key material from Keypair bytes"
      encryption: "RSA-2048 public key"
      encoding: "Base64 ciphertext in memo data"

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "PyPI"
      packages:
        - "semantic-types==0.1.5"
        - "semantic-types==0.1.6"
        - "solana-keypair"
        - "solana-publickey"
        - "solana-mev-agent-py"
        - "solana-trading-bot"
        - "soltrade"
      environments:
        - "developer virtualenvs"
        - "CI jobs"
        - "Jupyter notebooks"
        - "container images"
        - "Solana bots and trading tools"
      secret_material:
        - "Solana private keys generated by patched methods"
        - "Solana private keys imported through patched methods"
    not_currently_known_to_affect:
      - "the solders package itself"
      - "Solana keypairs generated in clean environments without the malicious packages"

    Indicators of Compromise

    package_versions:
      - "semantic-types==0.1.5"
      - "semantic-types==0.1.6"
      - "solana-keypair"
      - "solana-publickey"
      - "solana-mev-agent-py"
      - "solana-trading-bot"
      - "soltrade"
    domains:
      - "api.devnet.solana.com"
    urls:
      - "https://api.devnet.solana.com"
    solana:
      memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
      actor_public_key: "D782zqWjgSvy4hQoqzY1ySrGrotnXm1suJeXFur8sAko"
    crypto:
      rsa_public_key_fingerprint_sha256: "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62"
    python_symbols:
      - "solders.keypair.Keypair.from_seed"
      - "solders.keypair.Keypair.from_bytes"
      - "solders.keypair.Keypair.from_base58_string"

    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-semantic-types-pypi-solana-monkey-patch-scope"))
    SINCE = "2025-01-26T00:00:00Z"
    UNTIL = "2025-05-29T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
      "0.1.5",
      "0.1.6",
      "semantic-types==0.1.5",
      "semantic-types==0.1.6",
      "solana-keypair",
      "solana-publickey",
      "solana-mev-agent-py",
      "solana-trading-bot",
      "soltrade",
    ]
    FILES = [
    ]
    DOMAINS = [
      "api.devnet.solana.com",
      "solders.keypair.Keypair",
    ]
    URLS = [
      "https://api.devnet.solana.com",
    ]
    IPS = [
    ]
    HASHES = [
      "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62",
    ]
    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 = "2025-01-26T00:00:00Z"
    UNTIL = "2025-05-29T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-semantic-types-pypi-solana-monkey-patch-github-audit"))
    
    SELECTORS = [
      "0.1.5",
      "0.1.6",
      "semantic-types==0.1.5",
      "semantic-types==0.1.6",
      "solana-keypair",
      "solana-publickey",
      "solana-mev-agent-py",
      "solana-trading-bot",
      "soltrade",
      "api.devnet.solana.com",
      "solders.keypair.Keypair",
      "https://api.devnet.solana.com",
      "5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62",
    ]
    
    # 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 = "2025-01-26T00:00:00Z"
    UNTIL = "2025-05-29T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-semantic-types-pypi-solana-monkey-patch-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 = "2025-01-26T00:00:00Z"
    OUT = Path(os.environ.get("OUT", "hp-semantic-types-pypi-solana-monkey-patch-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
      "0.1.5",
      "0.1.6",
      "semantic-types==0.1.5",
      "semantic-types==0.1.6",
      "solana-keypair",
      "solana-publickey",
      "solana-mev-agent-py",
      "solana-trading-bot",
      "soltrade",
    ]
    
    # 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: Monkey-Patched PyPI Packages Use Transitive Dependencies to Steal Solana Private Keys - Role: PRIMARY_RESEARCH - Impact: Package cluster, malicious versions, payload behavior, patched methods, Solana Devnet exfiltration, actor key, RSA fingerprint, and PyPI publisher identity.

    IOC Clipboard

    4 IOCs
    Defang IOCs
    domain api.devnet.solana.com api[.]devnet[.]solana[.]com
    domain solders.keypair.Keypair solders[.]keypair[.]Keypair
    url https://api.devnet.solana.com hxxps://api[.]devnet[.]solana[.]com
    hash 5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62 5a4d8480c9d1e82ba102f200258882fb9e694e8fc0343b6982c5540beccdca62