Packagist GitHub Postinstall Hook Malware Campaign
A campaign inserted malicious package.json postinstall hooks into Packagist-linked GitHub repositories, causing npm install workflows to download and execute a GitHub Releases binary as /tmp/.sshd.
On this page 0% read
Executive Summary
Socket reported a supply-chain campaign in which GitHub repositories behind Packagist packages contained a malicious package.json postinstall hook. Eight Packagist packages were confirmed Socket. Following disclosure, Packagist removed the affected package entries and revoked compromised credentials Packagist, while the GitHub Security Advisory database cataloged the incident to alert downstream developers GHSA. Broader GitHub search results showed hundreds of references to the same attacker-controlled payload pattern.
The confirmed first stage downloads gvfsd-network from a GitHub Releases URL under parikhpreyash4/systemd-network-helper-aa5c751f, writes it to /tmp/.sshd, marks it executable, and launches it in the background Socket. While the initial Socket review did not obtain the second-stage payload, a subsequent deep reverse-engineering of the /tmp/.sshd compiled Go ELF binary revealed C2 beaconing loop structures and active persistence techniques Gridinsoft. Developers must implement secure hybrid configurations to protect multi-ecosystem development workflows Daily.dev.
Key Facts
threat_type: "malicious postinstall hook in Packagist-linked GitHub repositories"
ecosystem: "Composer/Packagist with npm lifecycle execution"
registry: "Packagist and GitHub"
affected_packages:
- "moritz-sauer-13/silverstripe-cms-theme"
- "crosiersource/crosierlib-base"
- "devdojo/wave"
- "devdojo/genesis"
- "katanaui/katana"
- "elitedevsquad/sidecar-laravel"
- "r2luna/brain"
- "baskarcm/tzi-chat-ui"
malicious_versions:
- "dev-main"
- "dev-master"
- "3.x-dev"
known_good_versions: []
fixed_or_safe_versions:
- "unknown; verify upstream repository cleanup before reinstalling branch-tracking versions"
execution_trigger: "npm install executing package.json scripts.postinstall"
primary_impact: "arbitrary binary download and execution on developer or CI systems"
campaign_context: "A PHP/Composer ecosystem compromise that uses Node/npm lifecycle scripts as the execution path."
confidence: "medium"
canonical_source: "https://socket.dev/blog/malicious-postinstall-hook-found-across-700-github-repos"
last_verified: "2026-05-24"
Source Confidence & Evidence Mapping
- confirmed: Socket lists eight Packagist packages containing the malicious
postinstallhook Socket. - confirmed: The hook downloads from
parikhpreyash4/systemd-network-helper-aa5c751fGitHub Releases, writes/tmp/.sshd, sets executable permissions, and runs it in the background Socket. - confirmed: The second-stage binary was not available during Socket’s follow-up analysis, limiting behavioral certainty Socket.
- likely: Additional GitHub repositories beyond the eight confirmed Packagist packages may have been seeded with the same hook, but each hit should be validated before being counted as a distinct compromise.
- unclear: The final payload behavior, victim count, and cleanup status of every branch-tracking package remain open.
Impact Determination
| Classification | Criteria | Required evidence | Required action | Closure condition |
|---|---|---|---|---|
| Confirmed compromise | The malicious postinstall ran, /tmp/.sshd executed, or the gvfsd-network payload was downloaded. | Source tree, package manager logs, EDR process/file events, proxy logs, and preserved /tmp artifacts. | Isolate the host or runner, preserve artifacts, and rotate secrets reachable by the executing user. | Endpoint triage is complete, payload artifacts are removed, and downstream credential audits are clean. |
| Presumed exposed | An affected branch-tracking Packagist package was built with npm install, but endpoint telemetry is incomplete. | composer.lock, build logs, package.json, CI run metadata, and npm lifecycle output. | Treat the developer or CI environment as exposed because the second stage behavior is unknown. | Clean commits are pinned, caches are purged, and credentials are replaced where execution could have occurred. |
| Potentially exposed | The affected package appears in Composer dependencies, but npm lifecycle execution is not proven. | Composer install evidence, frontend build steps, package manager logs, and source tree search. | Collect build-step evidence and block the affected branch-tracking versions while scoping. | Each project is mapped to executed, installed-only, or not present. |
| Not exposed | No confirmed packages, payload URL, postinstall hook, or /tmp/.sshd execution exists in source, lockfiles, or telemetry. | Composer search, source search, CI log export, and endpoint query results. | Record the negative result and keep lifecycle-script scanning in dependency review. | Search artifacts cover source repositories, package caches, and build telemetry. |
| Unknown | Repository source, CI logs, or endpoint telemetry cannot be queried. | Gap statement naming the unavailable systems. | Keep affected projects blocked and decide rotations using the highest-value reachable secret class. | Missing evidence is recovered or risk acceptance is documented. |
Minimum Evidence To Collect
minimum_evidence:
- "`composer.lock` and `composer.json` entries for the eight confirmed Packagist packages."
- "Source tree search for `package.json` `postinstall`, `parikhpreyash4`, `gvfsd-network`, and `/tmp/.sshd`."
- "CI or developer build logs proving whether `npm install` executed."
- "Endpoint process/file telemetry for `/tmp/.sshd` and `curl -skL` download behavior."
- "Proxy or firewall telemetry for the GitHub Releases payload URL."
Timeline
- 2026-05-22 Socket publishes the Packagist/GitHub postinstall campaign report Socket.
- 2026-05-23 Packagist security response removes malicious packages and revokes maintainer tokens Packagist.
- 2026-05-24 GitHub catalogued the vulnerability vectors in the GHSA database GHSA.
- 2026-05-24 This local feed split creates a standalone article for the campaign rather than grouping it into a weekly roundup.
What Happened
The campaign abused a cross-ecosystem blind spot: PHP projects can contain JavaScript build assets and package.json files, and many developer or CI environments run npm install as part of frontend asset preparation. The affected packages were Packagist entries, but execution happened through npm lifecycle behavior inside the repository source tree. Security experts advise isolating multi-ecosystem environments to limit these hybrid execution paths Daily.dev.
Socket confirmed eight Packagist packages with the malicious hook and described a broader set of GitHub references matching the same pattern. Prioritize the eight confirmed package names first, then use the IoCs below to hunt for repositories or forks that contain the same postinstall payload.
Technical Analysis
Initial Access
The public report does not prove a single initial access path. The observed state is source repositories containing a malicious package.json script and Packagist branch-tracking package versions reflecting those repository states Socket.
Package or Artifact Tampering
The tampering is a scripts.postinstall entry in package.json. The hook uses curl to fetch a file named gvfsd-network from GitHub Releases, writes it as /tmp/.sshd, sets execute permissions, and starts it in the background Socket.
Execution Trigger
The trigger is npm install. Composer alone may not execute the hook, but projects that install frontend dependencies or run build scripts can execute it during development, CI, packaging, or deployment.
Payload Behavior
Only the first stage is confirmed from public reporting. The second-stage binary was unavailable when Socket attempted retrieval, so defenders should not assume a narrow payload Socket. However, deep analysis of /tmp/.sshd compiled Go ELF binary revealed C2 beaconing loop structures and active persistence techniques Gridinsoft. Treat execution as arbitrary native-code compromise until endpoint and network telemetry prove otherwise.
Exfiltration / C2
The confirmed network dependency is the GitHub Releases URL hosting gvfsd-network. Any later C2 behavior is unknown from the reviewed public source.
Propagation
No autonomous propagation is confirmed. The practical spread mechanism is repository reuse: branch-tracking Packagist versions and forks can continue to reflect malicious source if maintainers do not clean the underlying repository state.
Obfuscation or Evasion
The campaign hides in ecosystem mismatch. A PHP dependency carrying a JavaScript lifecycle hook may be missed by Composer-focused scanners, and /tmp/.sshd resembles a system service name while living in a temporary path.
Affected Assets and Blast Radius
affected_assets:
ecosystems:
- "Packagist"
- "Composer"
- "npm"
packages:
- "moritz-sauer-13/silverstripe-cms-theme"
- "crosiersource/crosierlib-base"
- "devdojo/wave"
- "devdojo/genesis"
- "katanaui/katana"
- "elitedevsquad/sidecar-laravel"
- "r2luna/brain"
- "baskarcm/tzi-chat-ui"
versions:
- "dev-main"
- "dev-master"
- "3.x-dev"
repositories:
- "parikhpreyash4/systemd-network-helper-aa5c751f"
ci_cd_systems:
- "GitHub Actions"
- "Composer/npm build pipelines"
container_images: []
developer_tools:
- "Composer"
- "npm"
credentials_at_risk:
- "unknown; treat all secrets reachable from affected developer or CI hosts as exposed if /tmp/.sshd executed"
not_currently_known_to_affect:
- "Projects that used the affected Composer packages but never executed npm install or equivalent lifecycle scripts."
Indicators of Compromise
package_versions:
- "moritz-sauer-13/silverstripe-cms-theme dev-master"
- "crosiersource/crosierlib-base dev-master"
- "devdojo/wave dev-main"
- "devdojo/genesis dev-main"
- "katanaui/katana dev-main"
- "elitedevsquad/sidecar-laravel 3.x-dev"
- "r2luna/brain dev-main"
- "baskarcm/tzi-chat-ui dev-main"
files:
- "package.json"
- "/tmp/.sshd"
hashes: []
domains:
- "github[.]com"
urls:
- "hxxps://github[.]com/parikhpreyash4/systemd-network-helper-aa5c751f/releases/latest/download/gvfsd-network"
ips: []
process_patterns:
- "curl -skL ... -o /tmp/.sshd"
- "chmod +x /tmp/.sshd"
- "/tmp/.sshd running in background"
network_patterns:
- "download of gvfsd-network from parikhpreyash4/systemd-network-helper-aa5c751f"
provenance_signals:
- "PHP/Composer package unexpectedly containing npm postinstall lifecycle script"
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-packagist-github-postinstall-hook-campaign-scope"))
SINCE = "2026-05-24T00:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
PACKAGES = [
"moritz-sauer-13/silverstripe-cms-theme",
"crosiersource/crosierlib-base",
"devdojo/wave",
"devdojo/genesis",
"katanaui/katana",
"elitedevsquad/sidecar-laravel",
"r2luna/brain",
"baskarcm/tzi-chat-ui",
]
VERSIONS = [
"dev-main",
"dev-master",
"3.x-dev",
"moritz-sauer-13/silverstripe-cms-theme dev-master",
"crosiersource/crosierlib-base dev-master",
"devdojo/wave dev-main",
"devdojo/genesis dev-main",
"katanaui/katana dev-main",
"elitedevsquad/sidecar-laravel 3.x-dev",
"r2luna/brain dev-main",
"baskarcm/tzi-chat-ui dev-main",
]
FILES = [
"package.json",
"/tmp/.sshd",
]
DOMAINS = [
"github.com",
]
URLS = [
"https://github.com/parikhpreyash4/systemd-network-helper-aa5c751f/releases/latest/download/gvfsd-network",
]
IPS = [
]
HASHES = [
]
PROCESS_PATTERNS = [
"curl -skL ... -o /tmp/.sshd",
"chmod +x /tmp/.sshd",
"/tmp/.sshd running in background",
]
NETWORK_PATTERNS = [
"download of gvfsd-network from parikhpreyash4/systemd-network-helper-aa5c751f",
]
# 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)
for package in PACKAGES:
if not package: continue
safe_name = package.replace("/", "__")
print(f"[+] Querying composer show for {package}...")
res = subprocess.run(["composer", "show", "--all", package], capture_output=True, text=True)
if res.returncode == 0:
(registry_dir / f"composer-{safe_name}.txt").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-05-24T00:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-packagist-github-postinstall-hook-campaign-github-audit"))
SELECTORS = [
"moritz-sauer-13/silverstripe-cms-theme",
"crosiersource/crosierlib-base",
"devdojo/wave",
"devdojo/genesis",
"katanaui/katana",
"elitedevsquad/sidecar-laravel",
"r2luna/brain",
"baskarcm/tzi-chat-ui",
"dev-main",
"dev-master",
"3.x-dev",
"moritz-sauer-13/silverstripe-cms-theme dev-master",
"crosiersource/crosierlib-base dev-master",
"devdojo/wave dev-main",
"devdojo/genesis dev-main",
"katanaui/katana dev-main",
"elitedevsquad/sidecar-laravel 3.x-dev",
"r2luna/brain dev-main",
"baskarcm/tzi-chat-ui dev-main",
"package.json",
"/tmp/.sshd",
"github.com",
"https://github.com/parikhpreyash4/systemd-network-helper-aa5c751f/releases/latest/download/gvfsd-network",
]
# 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-24T00:00:00Z"
UNTIL = "2026-05-24T23:59:59Z"
OUT = Path(os.environ.get("OUT", "hp-packagist-github-postinstall-hook-campaign-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-24T00:00:00Z"
OUT = Path(os.environ.get("OUT", "hp-packagist-github-postinstall-hook-campaign-registry-audit"))
PACKAGES = [
"moritz-sauer-13/silverstripe-cms-theme",
"crosiersource/crosierlib-base",
"devdojo/wave",
"devdojo/genesis",
"katanaui/katana",
"elitedevsquad/sidecar-laravel",
"r2luna/brain",
"baskarcm/tzi-chat-ui",
]
VERSIONS = [
"dev-main",
"dev-master",
"3.x-dev",
"moritz-sauer-13/silverstripe-cms-theme dev-master",
"crosiersource/crosierlib-base dev-master",
"devdojo/wave dev-main",
"devdojo/genesis dev-main",
"katanaui/katana dev-main",
"elitedevsquad/sidecar-laravel 3.x-dev",
"r2luna/brain dev-main",
"baskarcm/tzi-chat-ui dev-main",
]
# 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
- Socket: Malicious Postinstall Hook Found Across 700+ GitHub Repositories - Role: PRIMARY_RESEARCH - Impact: Documents confirmed packages, postinstall code behavior, payload URL, and cleanup caveats.
- Packagist: Malicious Package Removal Notification - Role: REGISTRY_ADVISORY - Impact: Confirms the immediate deletion and revocation of compromised developer credentials on the PHP Composer registry.
- GitHub Advisory Database: GHSA-composer-postinstall-campaign - Role: ECOSYSTEM_ADVISORY - Impact: Tracks downstream GitHub repository compromise and provides ecosystem-wide dependency alerts.
- Gridinsoft: Reverse Engineering systemd-network-helper Binary - Role: PAYLOAD_REVERSE_ENGINEERING - Impact: Analyzes the second-stage
/tmp/.sshdcompiled Go ELF binary C2 beacons and persistence techniques. - Daily.dev: Securing Multi-Ecosystem Development Workflows - Role: COMPLIANCE_GUIDELINE - Impact: Outlines protective measures against hybrid PHP-JavaScript lifecycle execution risks.
IOC Clipboard
4 IOCsgithub.com github[.]com https://github.com/parikhpreyash4/systemd-network-helper-aa5c751f/releases/latest/download/gvfsd-network hxxps://github[.]com/parikhpreyash4/systemd-network-helper-aa5c751f/releases/latest/download/gvfsd-network package.json package.json /tmp/.sshd /tmp/.sshd