critical Threat analysis

Bitwarden CLI npm 2026.4.0 Credential Stealer

Bitwarden confirmed that @bitwarden/[email protected] was maliciously distributed through the npm CLI delivery path for a short April 22, 2026 window. JFrog and Socket analysis tied the package to bw_setup.js, bw1.js, Bun bootstrap, audit.checkmarx.cx exfiltration, GitHub fallback channels, and developer/CI credential theft.

#npm#supply-chain#bitwarden#github-actions#credential-theft#ci-cd
On this page 0% read

    Executive Summary

    Bitwarden confirmed a malicious npm release of @bitwarden/[email protected] in the CLI npm delivery path on April 22, 2026. Bitwarden’s public statement narrows affected users to npm CLI installs during the vendor-stated window of 5:57 PM to 7:30 PM ET on April 22, 2026, and states that vault data, production data, and production systems were not found to be compromised [Source 1].

    JFrog analyzed the malicious package and found that the package rewired preinstall and the bw binary entrypoint to bw_setup.js, which bootstrapped Bun 1.3.13 and ran bw1.js. The payload targeted developer and CI credentials, exfiltrated to audit.checkmarx.cx/v1/telemetry, resolved the primary domain to 94.154.172.43, and used GitHub commit search/repository creation as fallback transport [Source 2]. Socket independently tracked the same package/version, endpoint, IP, lock file, and GitHub artifact/workflow abuse patterns [Source 3].

    The npm registry metadata still records a 2026.4.0 timestamp even though the removed version is absent from the current versions list. Use 2026-04-22T21:22:59Z to start collection and 2026-04-22T23:30:00Z as the initial end bound; classify exposure by exact package/version plus execution evidence, not by generic Bitwarden usage [Source 4].

    Key Facts

    event_type: "legitimate npm package delivery compromise"
    ecosystem: "npm"
    package:
      name: "@bitwarden/cli"
      malicious_version: "2026.4.0"
      clean_replacement_versions:
        - "2026.4.1"
        - "2026.4.2"
    collection_window_utc:
      start: "2026-04-22T21:22:59Z"
      vendor_affected_start: "2026-04-22T21:57:00Z"
      vendor_affected_end: "2026-04-22T23:30:00Z"
    execution_triggers:
      - "npm preinstall runs bw_setup.js"
      - "bw binary entrypoint points to bw_setup.js"
    payload_files:
      - "bw_setup.js"
      - "bw1.js"
    payload_hashes_sha256:
      bw_setup_js: "18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb"
      bw1_js: "8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14"
      tampered_root_metadata: "167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad"
    network_iocs:
      - "audit.checkmarx.cx"
      - "94.154.172.43"
      - "https://audit.checkmarx.cx/v1/telemetry"
    github_iocs:
      - "LongLiveTheResistanceAgainstMachines"
      - "beautifulcastle"
      - "Shai-Hulud: The Third Coming"
    runtime_iocs:
      - "github.com/oven-sh/bun/releases/download/bun-v1.3.13"
      - "bun-v1.3.13"
    credentials_at_risk:
      - "GitHub CLI tokens and PATs"
      - "npm tokens"
      - "SSH keys"
      - "AWS credentials"
      - "GCP credentials"
      - "Azure credentials"
      - "GitHub Actions secrets reachable through stolen tokens"
      - "AI and MCP tool configuration files"

    Source Confidence & Evidence Mapping

    • confirmed: Bitwarden publicly confirmed malicious distribution of @bitwarden/[email protected] through npm and limited the affected population to npm CLI users in the April 22, 2026 window [Source 1].
    • confirmed: JFrog identified bw_setup.js, bw1.js, the preinstall and bin.bw rewiring, Bun 1.3.13, audit.checkmarx.cx/v1/telemetry, 94.154.172.43, the GitHub fallback markers, and SHA-256 hashes for the loader, payload, and tampered metadata [Source 2].
    • confirmed: Socket reported the same package/version and called out audit.checkmarx.cx, 94.154.172.43, /tmp/tmp.987654321.lock, package update artifacts, Bun execution, and GitHub Actions artifact/workflow abuse patterns [Source 3].
    • unclear: Public sources do not prove which third-party action, token path, or repository state produced the malicious npm package. Treat CI/CD compromise mechanism claims beyond the observed package behavior as unresolved unless new vendor evidence appears.
    • not_observed: Bitwarden reported no evidence of end-user vault data access, production data compromise, or production system compromise [Source 1].

    Impact Determination

    ClassificationCriteriaEvidence to collectHandling decision
    Confirmed compromise@bitwarden/[email protected] executed and any payload, network, GitHub fallback, or credential access indicator appears.npm install output, lockfile/package cache, bw_setup.js, bw1.js, Bun 1.3.13, audit.checkmarx.cx, 94.154.172.43, LongLiveTheResistanceAgainstMachines, beautifulcastle, GitHub repo/artifact creation evidence.Isolate the host or runner, preserve package/cache/process/network evidence, revoke credentials present on that environment, and run the downstream audits below.
    Presumed exposed@bitwarden/[email protected] was installed or pulled on a developer host, container build, or CI job, but runtime/network telemetry is missing.Package manager cache, package-lock.json, npm registry proxy entries, CI job logs, image layer history, endpoint inventory.Treat credentials reachable from that process as exposed unless negative execution evidence is complete.
    Potentially exposed@bitwarden/cli appears in dependency manifests or install scripts and the resolved version during the April 22 window is unknown.Dependency manifests, historical lockfiles, package proxy records, CI log exports, build image SBOMs.Collect resolver/version evidence until the asset moves to confirmed compromise, presumed exposed, or not exposed.
    Not exposedEvidence shows no @bitwarden/[email protected] tarball, install, cache entry, image layer, process, or network selector in scope.Negative repository search, package proxy query, CI job export, endpoint search, and image/cache inventory.Keep the negative evidence with the case record and close this event for the asset.
    UnknownRequired package, CI, endpoint, proxy, or registry telemetry is unavailable for the April 22 collection window.A named telemetry gap with owner, system, and retention status.Keep high-value developer/CI assets in scope and decide credential revocation based on reachable secret inventory.

    Minimum Evidence To Collect

    package_evidence:
      - "@bitwarden/[email protected] in package-lock.json, yarn.lock, pnpm-lock.yaml, npm-shrinkwrap.json, npm cache, or package proxy records"
      - "npm registry metadata showing 2026.4.0 pulled by an internal cache or CI job"
    execution_evidence:
      - "bw_setup.js"
      - "bw1.js"
      - "bun-v1.3.13"
      - "/tmp/tmp.987654321.lock"
    network_evidence:
      - "audit.checkmarx.cx"
      - "94.154.172.43"
      - "https://audit.checkmarx.cx/v1/telemetry"
    github_evidence:
      - "LongLiveTheResistanceAgainstMachines"
      - "beautifulcastle"
      - "Shai-Hulud: The Third Coming"
      - "unexpected GitHub Actions workflow, artifact, branch, or repository creation from an exposed token"

    Timeline

    • 2026-04-22T21:22:59Z: npm registry metadata records @bitwarden/[email protected] in the package time map [Source 4].
    • 2026-04-22T21:57:00Z: Bitwarden’s affected-window statement starts at 5:57 PM ET [Source 1].
    • 2026-04-22T23:30:00Z: Bitwarden states the malicious npm delivery window ended at 7:30 PM ET [Source 1].
    • 2026-04-23: Bitwarden published the public notice and directed affected npm CLI users to uninstall @bitwarden/cli, clear npm cache, disable install scripts during cleanup, and install 2026.4.1 [Source 1].
    • 2026-04-23: JFrog published artifact-level analysis of bw_setup.js, bw1.js, the primary exfiltration URL, fallback GitHub paths, hashes, and targeted local paths [Source 2].
    • 2026-04-23: Socket published independent analysis of the same package/version and overlapping IOCs [Source 3].

    What Happened

    The malicious npm package kept Bitwarden CLI branding but changed the package execution path. JFrog observed a preinstall script of node bw_setup.js and a bin.bw value pointing to bw_setup.js, so both installation and direct CLI invocation could reach the malicious loader [Source 2].

    bw_setup.js checked for Bun, downloaded bun-v1.3.13 from github.com/oven-sh/bun when needed, and used Bun to execute bw1.js. bw1.js then collected local developer and CI credential material, encrypted the collected result set, and sent it to https://audit.checkmarx.cx/v1/telemetry with GitHub-based fallback paths if direct HTTPS exfiltration failed [Source 2].

    The GitHub abuse path matters for responders because the payload did not stop at local file theft. JFrog reports token validation against https://api.github.com/user, commit search for LongLiveTheResistanceAgainstMachines, fallback discovery using beautifulcastle, repository creation under a victim account, and GitHub Actions secret extraction through workflow execution and artifact retrieval [Source 2]. Socket also calls out workflow file creation and artifacts such as format-results.txt [Source 3].

    Technical Analysis

    Package Manipulation

    package_identity:
      registry: "npm"
      package: "@bitwarden/cli"
      malicious_version: "2026.4.0"
      modified_manifest_fields:
        scripts.preinstall: "node bw_setup.js"
        bin.bw: "bw_setup.js"
      mismatched_embedded_cli_version: "2026.3.0"

    Execution And Collection

    The execution chain is npm install or bw invocation to bw_setup.js, then Bun 1.3.13, then bw1.js. JFrog decoded credential targeting for gh auth token, GitHub and npm token patterns, environment variables, SSH paths, .git-credentials, .npmrc, .env, shell histories, AWS credentials, GCP credential DB files, and AI/MCP configuration paths [Source 2].

    Exfiltration

    primary_exfiltration:
      domain: "audit.checkmarx.cx"
      ip: "94.154.172.43"
      url: "https://audit.checkmarx.cx/v1/telemetry"
      encoding: "gzip plus RSA-OAEP-wrapped AES-256-GCM envelope"
    fallback_github_paths:
      - "https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50"
      - "https://api.github.com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc"

    Affected Assets and Blast Radius

    affected_assets:
      ecosystems:
        - "npm"
      packages:
        - "@bitwarden/[email protected]"
      developer_hosts:
        - "hosts that installed or ran @bitwarden/[email protected]"
      ci_cd_systems:
        - "runners that installed or ran @bitwarden/[email protected]"
      containers:
        - "images built while resolving @bitwarden/[email protected]"
      source_control:
        - "GitHub accounts and repositories reachable from stolen tokens"
      package_registries:
        - "npm accounts reachable from stolen npm tokens"
    not_currently_known_to_affect:
      - "Bitwarden web vault usage without npm CLI install"
      - "Bitwarden browser extension"
      - "Bitwarden server production systems per vendor statement"

    Indicators of Compromise

    package_versions:
      - "@bitwarden/[email protected]"
    files:
      - "bw_setup.js"
      - "bw1.js"
      - "/tmp/tmp.987654321.lock"
      - "package-updated.tgz"
    hashes_sha256:
      - "18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb"
      - "8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14"
      - "167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad"
    domains:
      - "audit.checkmarx.cx"
    ips:
      - "94.154.172.43"
    urls:
      - "https://audit.checkmarx.cx/v1/telemetry"
      - "https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50"
      - "https://api.github.com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc"
      - "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13"
    strings:
      - "LongLiveTheResistanceAgainstMachines"
      - "beautifulcastle"
      - "Shai-Hulud: The Third Coming"
      - "gh auth token"
    targeted_paths:
      - "~/.ssh/id_"
      - "~/.ssh/id*"
      - "~/.ssh/known_hosts"
      - "~/.ssh/keys"
      - ".git/config"
      - ".git-credentials"
      - "~/.npmrc"
      - ".npmrc"
      - ".env"
      - "~/.bash_history"
      - "~/.zsh_history"
      - "~/.aws/credentials"
      - "~/.config/gcloud/credentials.db"
      - "~/.claude.json"
      - ".claude.json"
      - "~/.claude/mcp.json"
      - "~/.kiro/settings/mcp.json"
      - ".kiro/settings/mcp.json"

    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-bitwarden-cli-npm-compromised-action-scope"))
    SINCE = "2026-04-22T21:22:59Z"
    UNTIL = "2026-04-22T23:59:59Z"
    
    PACKAGES = [
    ]
    VERSIONS = [
      "@bitwarden/[email protected]",
    ]
    FILES = [
      "bw_setup.js",
      "bw1.js",
      "/tmp/tmp.987654321.lock",
      "package-updated.tgz",
    ]
    DOMAINS = [
      "audit.checkmarx.cx",
      "tmp.987654321.lock",
      "api.github.com",
    ]
    URLS = [
      "https://audit.checkmarx.cx/v1/telemetry",
      "https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50",
      "https://api.github.com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc",
      "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13",
    ]
    IPS = [
      "94.154.172.43",
    ]
    HASHES = [
      "18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb",
      "8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14",
      "167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad",
    ]
    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 npm view for {package}...")
                res = subprocess.run(["npm", "view", package, "name", "version", "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
                if res.returncode == 0:
                    (registry_dir / f"npm-{safe_name}.json").write_text(res.stdout)
    
    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-04-22T21:22:59Z"
    UNTIL = "2026-04-22T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-bitwarden-cli-npm-compromised-action-github-audit"))
    
    SELECTORS = [
      "@bitwarden/[email protected]",
      "bw_setup.js",
      "bw1.js",
      "/tmp/tmp.987654321.lock",
      "package-updated.tgz",
      "audit.checkmarx.cx",
      "tmp.987654321.lock",
      "api.github.com",
      "https://audit.checkmarx.cx/v1/telemetry",
      "https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50",
      "https://api.github.com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc",
      "https://github.com/oven-sh/bun/releases/download/bun-v1.3.13",
      "94.154.172.43",
      "18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb",
      "8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14",
      "167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad",
    ]
    
    # 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-04-22T21:22:59Z"
    UNTIL = "2026-04-22T23:59:59Z"
    OUT = Path(os.environ.get("OUT", "hp-bitwarden-cli-npm-compromised-action-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-04-22T21:22:59Z"
    OUT = Path(os.environ.get("OUT", "hp-bitwarden-cli-npm-compromised-action-registry-audit"))
    PACKAGES = [
    ]
    VERSIONS = [
      "@bitwarden/[email protected]",
    ]
    
    # 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 npm dependencies in lockfiles/package.json
        print("[+] Scanning lockfiles for npm selectors...")
        for file in ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "package.json"]:
            if Path(file).exists():
                subprocess.run(["rg", "-n", "--hidden", "--fixed-strings", "-f", str(OUT / "affected-versions.txt"), file])
    
        # 2. Query registry metadata and fetch tarballs for local analysis
        metadata_dir = OUT / "metadata"
        tarballs_dir = OUT / "tarballs"
        metadata_dir.mkdir(exist_ok=True)
        tarballs_dir.mkdir(exist_ok=True)
        for package in PACKAGES:
            if not package: continue
            safe_name = package.replace("/", "__")
            print(f"[+] Querying npm view for {package}...")
            res = subprocess.run(["npm", "view", package, "time", "versions", "dist-tags", "maintainers", "dist.tarball", "dist.integrity", "scripts", "--json"], capture_output=True, text=True)
            if res.returncode == 0:
                (metadata_dir / f"npm-{safe_name}.json").write_text(res.stdout)
            subprocess.run(["npm", "pack", package, "--pack-destination", str(tarballs_dir)], capture_output=True)
    
        # 3. HOW TO REVOKE AND ROTATE EXPOSED NPM PUBLISHING TOKENS:
        # Revoke all compromised tokens via npm CLI:
        # subprocess.run(["npm", "token", "list"])
        # subprocess.run(["npm", "token", "revoke", "123456"])
        # Or logout to revoke the current session:
        # subprocess.run(["npm", "logout"])
        # Generate a new publishing token with MFA protection:
        # subprocess.run(["npm", "token", "create", "--read-only=false", "--cidr=0.0.0.0/0"])
    
    print(f"[+] Wrote registry audit artifacts under {OUT}")

    Sources

    1. Bitwarden Community Forums: Bitwarden Statement on Checkmarx Supply Chain Incident - Role: DIRECT_SOURCE - Impact: Vendor scope, affected window, non-impact statements, cleanup package version.
    2. JFrog Security Research: TeamPCP Campaign Spreads to npm via a Hijacked Bitwarden CLI - Role: PRIMARY_RESEARCH - Impact: Package manifest rewiring, loader/payload files, hashes, exfiltration, GitHub fallback selectors, credential targets.
    3. Socket: Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign - Role: PRIMARY_RESEARCH - Impact: Independent IOC set and GitHub Actions workflow/artifact abuse context.
    4. npm registry metadata for @bitwarden/cli - Role: REGISTRY_METADATA - Impact: Current versions list and time metadata for removed 2026.4.0.

    IOC Clipboard

    15 IOCs
    Defang IOCs
    domain audit.checkmarx.cx audit[.]checkmarx[.]cx
    domain tmp.987654321.lock tmp[.]987654321[.]lock
    domain api.github.com api[.]github[.]com
    url https://audit.checkmarx.cx/v1/telemetry hxxps://audit[.]checkmarx[.]cx/v1/telemetry
    url https://api.github.com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50 hxxps://api[.]github[.]com/search/commits?q=LongLiveTheResistanceAgainstMachines&sort=author-date&order=desc&per_page=50
    url https://api.github.com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc hxxps://api[.]github[.]com/search/commits?q=beautifulcastle%20&sort=author-date&order=desc
    url https://github.com/oven-sh/bun/releases/download/bun-v1.3.13 hxxps://github[.]com/oven-sh/bun/releases/download/bun-v1[.]3[.]13
    ip 94.154.172.43 94[.]154[.]172[.]43
    hash 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb
    hash 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14
    hash 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad
    file bw_setup.js bw_setup.js
    file bw1.js bw1.js
    file /tmp/tmp.987654321.lock /tmp/tmp.987654321.lock
    file package-updated.tgz package-updated.tgz