On 8 October 2026 at 01:12 UTC, tensorlake 0.5.144 reached npm carrying a Shai-Hulud style worm. Three days earlier, on 5 October at 11:56 UTC, @subql/common 5.8.3 shipped a credential stealer. Both versions are gone from the registry now, but the first question after an advisory like this never changes: is that exact version anywhere in our organization? Here is a sweep you can run in minutes with gh, jq and npm.
What's affected
A bad version can sit in four places, and each needs a different check:
- Lockfiles on default branches. Your GitHub dependency graph knows about these.
- Lockfiles on other branches, including bot branches that bump ranges. The dependency graph and code search only see the default branch.
- CI runs that resolved the version during the window, for example a job that runs
npm installwithout a committed lockfile. - Caches on runners, developer machines and internal registry mirrors, which can still serve a tarball the public registry removed.
The windows are short. Registry metadata shows tensorlake 0.5.144 published at 01:12 UTC on 8 October, with npm's 0.0.1-security placeholder at 04:15 UTC. Clean releases resumed with 0.5.145. For @subql/common, 5.8.2 is latest again. Watch for indirect paths: the current @subql/cli 6.6.3 and @subql/node-core 19.3.1 both depend on @subql/common with the range ~5.8.2, which accepted 5.8.3.
How to check if you're exposed
1. List every repository in the org.
ORG=your-org
gh api --paginate "orgs/$ORG/repos?per_page=100" \
--jq '.[] | select(.archived | not) | .full_name' > repos.txt
2. Export an SBOM per repository. GitHub's dependency graph exports SPDX JSON. The synchronous endpoint is the quickest:
mkdir -p sboms
while read -r repo; do
gh api "repos/$repo/dependency-graph/sbom" > "sboms/${repo//\//__}.json" 2>/dev/null \
|| echo "$repo" >> no-sbom.txt
done < repos.txt
GitHub's docs say this endpoint "will not be accessible after November 13, 2026." The replacement is asynchronous: request a report, poll the returned sbom_url while it answers 202, and follow the 302 to download.
url=$(gh api "repos/$repo/dependency-graph/sbom/generate-report" --jq .sbom_url)
while [ "$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $(gh auth token)" "$url")" = 202 ]; do
sleep 5
done
curl -sL -H "Authorization: Bearer $(gh auth token)" "$url" -o "sboms/${repo//\//__}.json"
Anything in no-sbom.txt needs the local check in step 4.
3. Search the SBOMs with jq. The SPDX packages array carries name and versionInfo for each dependency:
hunt() {
jq -r --arg n "$1" --arg v "$2" '(.sbom // .).packages[]
| select((.name | test("(^|:)" + $n + "$")) and .versionInfo == $v)
| "\(input_filename)\t\(.name)@\(.versionInfo)"' sboms/*.json
}
hunt tensorlake 0.5.144
hunt '@subql/common' 5.8.3
Then run a quick code search as a cross-check:
gh search code tensorlake --owner "$ORG" --filename package-lock.json --json url --jq '.[].url'
The gh manual notes this uses GitHub's legacy code search engine, and the REST docs say it covers only the default branch and files smaller than 384 KB. Large lockfiles slip past it, so treat it as a supplement.
4. Check local clones and other branches. In any checkout, ask npm directly from the lockfile:
npm query '[name="tensorlake"][version="0.5.144"], [name="@subql/common"][version="5.8.3"]' \
--package-lock-only --no-expect-results
--no-expect-results makes npm exit with an error when anything matches, so the same line works as a CI gate. For yarn and pnpm projects, grep the lockfiles:
grep -rnE 'tensorlake(@|-|/)0\.5\.144|@subql/common(@|/-/common-)5\.8\.3' \
--include=yarn.lock --include=pnpm-lock.yaml --include=package-lock.json .
On runners and laptops, check the npm cache with npm cache ls tensorlake@0.5.144.
5. Check CI runs inside the window. gh run list filters by creation time using GitHub's date syntax:
gh run list --repo "$repo" --created '2026-10-08T01:00:00Z..2026-10-08T05:00:00Z' \
--limit 200 --json databaseId,headSha,workflowName,createdAt
gh run view <run-id> --repo "$repo" --log | grep -nE 'tensorlake|@subql/common'
npm's default output rarely prints versions, so pair each run with its commit and read the lockfile it installed from: git show <headSha>:package-lock.json | grep -c 'tensorlake-0.5.144'. On npm 12 there is one more clue. Installs end with a list of packages whose install scripts were skipped, and both bad versions declared install scripts. Run logs expire, so do this step first if the incident is a few weeks old.
What to do now
- Run steps 1 to 3 today and save
repos.txt, the SBOMs and the hits. That record answers the auditor's question later. - Pin and regenerate. Move hits to a clean version (
tensorlake0.5.143 or 0.5.145 and later,@subql/common5.8.2), regenerate the lockfile and clear runner caches. - Treat any machine that installed the bad version as compromised. GitHub's advisory for
tensorlakesays to rotate all secrets "from a different computer." StepSecurity warns that itsgh-token-monitorservice wipes the home directory if the stolen token is revoked while it runs, so remove the monitor first, then revoke. - Add the npm query line to CI with both versions, and keep the list current as new advisories land.
- Script the async SBOM flow now, before the synchronous endpoint closes on 13 November.
Why it matters
Malicious versions now get pulled within hours, which makes it tempting to conclude nobody was exposed. The registry forgets fast. Your lockfiles, caches and CI history don't, and they are where the evidence is.
A sweep that depends on someone remembering every repo will miss the archived service that still deploys weekly. One that starts from the org's repository list and a machine-readable SBOM per repo won't.
Try it yourself
Export the SBOM for one repository you own and run the hunt function against it with a package and version you know is there, so you can trust a clean result. The SPDX file also works in the free browser scan at https://gold.safeguard.sh/scan, which runs locally, accepts CycloneDX and SPDX SBOMs and checks up to 100 dependencies against known malicious packages.
Click through it at your own pace, or open it full screen.
Quick check
Why isn't GitHub code search enough on its own?
Through the API it uses the legacy engine, covers only the default branch and skips files of 384 KB or more, which excludes many lockfiles.
What changes on 13 November 2026?
GitHub's synchronous SBOM export endpoint stops working. Scripts must request a report with generate-report and then poll the returned sbom_url.
Can a removed version still reach your builds?
Yes. Runner caches, developer npm caches and internal mirrors can keep serving a tarball after the public registry drops it.
Sources
- StepSecurity: tensorlake npm package compromised
- StepSecurity: SubQuery ecosystem compromised
- GitHub Advisory GHSA-rqxj-g25x-4v9v
- OSV: MAL-2026-17650
- OSV: MAL-2026-17571
- npm registry metadata for tensorlake
- npm registry metadata for @subql/common
- GitHub REST API: SBOM endpoints
- GitHub Docs: Exporting an SBOM for your repository
- GitHub REST API: Repositories
- GitHub REST API: Search
- GitHub Docs: Understanding the search syntax
- GitHub CLI manual: gh api, gh search code, gh run list, gh run view
- npm docs: npm query, npm cache, npm install-scripts