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.
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, thepreinstallandbin.bwrewiring, Bun1.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
| Classification | Criteria | Evidence to collect | Handling 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 exposed | Evidence 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. |
| Unknown | Required 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 install2026.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
- Bitwarden Community Forums: Bitwarden Statement on Checkmarx Supply Chain Incident - Role: DIRECT_SOURCE - Impact: Vendor scope, affected window, non-impact statements, cleanup package version.
- 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.
- Socket: Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign - Role: PRIMARY_RESEARCH - Impact: Independent IOC set and GitHub Actions workflow/artifact abuse context.
- npm registry metadata for @bitwarden/cli - Role: REGISTRY_METADATA - Impact: Current versions list and
timemetadata for removed2026.4.0.
IOC Clipboard
15 IOCsaudit.checkmarx.cx audit[.]checkmarx[.]cx tmp.987654321.lock tmp[.]987654321[.]lock api.github.com api[.]github[.]com https://audit.checkmarx.cx/v1/telemetry hxxps://audit[.]checkmarx[.]cx/v1/telemetry 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 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 https://github.com/oven-sh/bun/releases/download/bun-v1.3.13 hxxps://github[.]com/oven-sh/bun/releases/download/bun-v1[.]3[.]13 94.154.172.43 94[.]154[.]172[.]43 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb 18f784b3bc9a0bcdcb1a8d7f51bc5f54323fc40cbd874119354ab609bef6e4cb 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14 8605e365edf11160aad517c7d79a3b26b62290e5072ef97b102a01ddbb343f14 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad 167ce57ef59a32a6a0ef4137785828077879092d7f83ddbc1755d6e69116e0ad bw_setup.js bw_setup.js bw1.js bw1.js /tmp/tmp.987654321.lock /tmp/tmp.987654321.lock package-updated.tgz package-updated.tgz