nrwl

Activity

Stars and mentions · Last 30 days, Oct 9 – Nov 7

GitHub stars6
Mentions7
01234Oct 9Oct 23Nov 7
UTC days · repositories nrwl owns4 repositories

Top repositories

nrwl’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside nrwl’s account where nrwl opened or reviewed pull requests in the past 365 days.

Who noticed nrwl

Everyone who starred or forked nrwl’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

7posts
Nov 80 posts
  1. Trust No Pipeline: Why Attackers Stopped Hacking Your Cluster and Started Riding Your CI/CD (And How to Stop Them on Google Cloud) Sourced from: my talk "Trust No Pipeline: Securing Software Delivery & Workload Identity in the Cloud" (slides), the companion live demo repo, and the public 2025–2026 incident write-ups linked at the bottom. --- So here is the thing. We spent the last decade turning our production clusters into fortresses. Private GKE nodes. VPCs. Cloud Armor. IAM policies reviewed by three different teams. Firewalls on top of firewalls. And attackers looked at all of that and said: "Cool. We will not touch it." Why would they? Why fight the fortress when you can hijack the delivery truck? Your CI/CD pipeline is that truck. It drives through the front gate every ten minutes. Nobody checks it at the gate, because it is your truck. It carries roles/owner. It runs preinstall hooks from packages you have never read. It pulls GitHub Actions by mutable @v1 tags. And somewhere in its glovebox there is probably a Service Account JSON key that expires in the year 9999. That is the whole plot twist of modern supply chain attacks. Nobody is breaking in. They are riding in. And here is the kicker: this is not a theoretical "what if" talk. Every pattern in this post comes from a real incident in 2025 or 2026. Tens of thousands of repositories. Hundreds of npm packages. Billions of monthly downloads. All of it exploiting the same thing: we trust our pipelines way more than we should. So let us stop doing that. --- The Bait and Switch That Hit 23,000 Repos Overnight Let me start with the one that made everyone pay attention. March 2025. CVE-2025-30066. tj-actions/changed-files, a GitHub Action used by more than 23,000 repositories. The attackers did not hack Google Cloud. They did not hack AWS. They did not even hack the victims. They stole a bot token, and used it to repoint every single version tag of the action -- @v1 all the way through @v45 -- to a malicious commit. That commit ran a little Python script called memdump.py. It read /proc/$pid/mem of the Runner.Worker process and dumped everything it found straight into the build logs. Cleartext. GCP Service Account JSON keys, AWS keys, npm tokens, GitHub tokens. In public logs, for a lot of open-source projects. If your workflow said uses: tj-actions/changed-files@v45, you were not running the code you reviewed. You were running whatever the tag pointed to that morning. How they actually got there: six steps from SpotBugs to Coinbase What makes this one fascinating is that it was not a single hack. It was a cascade: 1. SpotBugs PR trap. A maintainer PAT was added to spotbugs/sonar-findbugs, then stolen through a malicious pull_request_target PR. 2. Rogue invite and leak. On March 11, that PAT invited a rogue user (jurkaofavak) to spotbugs/spotbugs. Two minutes later a workflow leaked the Reviewdog maintainer's PAT, AES + RSA encrypted, so nobody would notice it in the logs. 3. reviewdog/action-setup@v1 poisoned. The leaked PAT overwrote the mutable v1 tag. 4. Pivot to tj-actions. tj-actions/eslint-changed-files used reviewdog/action-setup@v1 as a transitive dependency, which exposed the tj-actions privileges. 5. The actual target: coinbase/agentkit. Five days after Coinbase added tj-actions/changed-files, the attacker forked the relevant repos to stage the attack. 6. All @v1–@v45 repointed. And 23,000+ repositories came along for the ride. Read that list again. Every single hop is a pipeline trusting another pipeline. Nobody exploited a buffer overflow. Nobody needed a zero-day. Mutable tags and over-privileged tokens did all the work. --- Then the Worms Showed Up If tj-actions was a targeted heist, 2025 and 2026 gave us something worse: self-propagating supply chain worms. Shai-Hulud 2.0 (Sept–Nov 2025) ran inside preinstall hooks, fired up an embedded TruffleHog scanner to harvest cloud credentials (GOOGLE_CREDENTIALS, AWS keys, Vault tokens), then automatically republished trojanized versions of every package the victim owned. As a bonus, it registered persistent rogue self-hosted GitHub Actions runners named SHA1HULUD. Mini Shai-Hulud (May 2026) was the streamlined sequel. Triggered via pull_request_target ("pwn requests"), it poisoned pnpm caches and dumped OIDC tokens directly from CI runner memory, hitting ecosystems including TanStack, Mistral AI, and UiPath. ChainDrop (August 2026) compromised core JavaScript maintainer accounts -- most notably keyv (think flat-cache, cacheable-request) -- and spread to 400+ npm packages and 1,300+ versions. It used a preinstall hook (setup.mjs) to drop an obfuscated Bun payload on Linux, macOS, and Windows, and routed its command-and-control through Ethereum smart contracts so corporate DNS blocklists had nothing to block. It then committed malicious updates under the dummy author claude with the very innocent message chore: update config. And the most uncomfortable lesson from ChainDrop? SLSA Level 2 did not save anyone. The maintainer's credentials were compromised before the build, so the provenance looked perfectly valid. The build system faithfully attested that it built the malware. What do all of these have in common? They eat keys. Static keys, long-lived tokens, OIDC tokens sitting in runner memory. Starve them of keys and they stop being worms and start being very sad scripts. --- The Four (Okay, Five) Pillars The rest of this post is the defense. I structured the talk around four pillars, plus a "pillar zero" that I think gets ignored way too often. For each one I will show the idea, the actual config, and the attack from the demo repo failing against it. --- Pillar 0: A Stolen Token Should Not Equal a Valid Commit Here is something that does not get talked about enough. Anyone can create a commit with someone else's name and email: git commit --author="Trusted Maintainer <maintainer@example.com>" -m "chore: update config" Git will happily accept it. GitHub will happily display it. That is exactly how ChainDrop's claude commits blended in. What we actually care about is authenticity (who really made this commit?) and integrity (has it been tampered with since?). Commit signing gives you both. If a signed commit is modified, the signature breaks immediately. But here is where it gets good. If you enforce that only signatures from maintainers' physical YubiKey / FIDO keys are accepted, then a stolen GitHub PAT is no longer enough to land code. The attacker has your token. They do not have the piece of hardware on your keychain. # 1. Lock Git to the maintainer's YubiKey GPG key git config --global user.signingkey 0x9F8B1234 git config --global commit.gpgsign true git config --global tag.gpgSign true # 2. Every commit now requires a physical touch on the key git commit -S -m "feat: release v1.0.0" Then enforce it on both ends: turn on Settings → Rules → Require signed commits in GitHub, and verify against a known keyring in CI: - name: Verify Maintainer Hardware GPG run: | gpg --import .github/maintainer-yubikeys.asc git verify-commit HEAD Stolen PAT without the YubiKey? Rejected. --- Pillar 1: Kill Static Secrets (Vampire Keys vs. Melting Ice Cubes) Why did Shai-Hulud and ChainDrop do so much damage? Because teams still export GCP Service Account JSON keys. A JSON key is a vampire. It lives until December 31, 9999 unless somebody drives a stake through its heart. It sits in a GitHub secret, gets loaded into the runner's environment, and waits for the first preinstall hook with a TruffleHog binary to come along and grab it. An ephemeral token is an ice cube. It is issued on demand and melts in about an hour. Even if it ends up dumped in a public log, by the time someone finds it, it is a puddle. On Google Cloud, you can kill the vampires for your entire project or organization with one Organization Policy. Five lines of YAML: # disable-sa-keys.yaml name: projects/${PROJECT_ID}/policies/iam.disableServiceAccountKeyCreation spec: rules: - enforce: true # 1. Audit what is already out there gcloud iam service-accounts keys list \ --iam-account="ci-deployer@${PROJECT_ID}.iam.gserviceaccount.com" \ --managed-by=user \ --format="table(name.basename(), validAfterTime, validBeforeTime, keyAlgorithm)" # 2. Block key creation going forward gcloud org-policies set-policy disable-sa-keys.yaml While you are there, also look at iam.disableServiceAccountKeyUpload, and check IAM & Admin → Service Accounts → Keys for anything with that lovely 9999 expiry date. The Recommender insights (google.iam.serviceAccount.Insight) will tell you which keys and accounts have not been used in ages. The demo: trying to mint a backdoor key Attack vector 2 in the demo plays the attacker who already compromised the CI identity and wants persistence. The obvious move is to mint a long-lived key: gcloud iam service-accounts keys create /tmp/backdoor-key.json \ --iam-account=ci-prod-deployer@${PROJECT_ID}.iam.gserviceaccount.com With the Org Policy enforced, GCP says no. No key, no persistence, no year-9999 souvenir. --- Pillar 2: Workload Identity Federation (a.k.a. Passport Control) "Okay, no JSON keys. Then how does my GitHub Actions workflow talk to Google Cloud?" Workload Identity Federation (WIF). And the easiest way I have found to explain it is airport passport control: 1. The CI runner gets a signed OIDC JWT from GitHub for that specific job. That is the passport. 2. Google Cloud STS is customs. It checks the signature and the claims inside the passport against your rules. 3. If everything checks out, STS hands back a visitor badge -- a short-lived token valid for roughly 15–60 minutes. There is nothing stored in GitHub. Nothing to steal from a secrets store. Nothing that lives until 9999. So everybody migrated to WIF and lived happily ever after, right? The #1 OIDC mistake: who is on the VIP list? Not quite. This is where Nx s1ngularity (August 2025) and Mini Shai-Hulud come back in. A lot of teams moved to WIF and configured their provider to check exactly one thing: assertion.repository == 'acme/app' Some checked only repository_owner, which is even worse. The problem? A pull_request_target workflow, an unmerged feature branch, or a job on a rogue self-hosted runner still carries "repository": "acme/app" in its OIDC token. So your customs officer checks the passport, sees the right country, and waves through an untrusted PR to mint a production GCP token. You moved away from static keys and then handed out VIP badges to anyone who can open a pull request. The fix is a strict CEL attribute condition on the provider. Check the immutable numeric owner ID (it survives org renames), the exact repo, the protected branch, the event type, and -- because of SHA1HULUD -- the runner environment: gcloud iam workload-identity-pools create "github-prod-pool" \ --project="${PROJECT_ID}" \ --location="global" \ --display-name="GitHub Actions Prod Pool" gcloud iam workload-identity-pools providers create-oidc "github-oidc-provider" \ --project="${PROJECT_ID}" \ --location="global" \ --workload-identity-pool="github-prod-pool" \ --display-name="GitHub Actions Hardened OIDC" \ --issuer-uri="https://token.actions.githubusercontent.com" \ --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.ref=assertion.ref,attribute.workflow_ref=assertion.job_workflow_ref" \ --attribute-condition="assertion.repository_owner_id == '${GITHUB_ORG_ID}' && assertion.repository == '${GITHUB_REPO}' && assertion.ref == 'refs/heads/main' && assertion.event_name == 'push' && assertion.runner_environment == 'github-hosted'" You can tighten it further with assertion.job_workflow_ref to pin the exact workflow file allowed to deploy. And you do not even need an intermediate service account anymore. Grant the role directly to the federated principal: gcloud artifacts repositories add-iam-policy-binding "prod-containers" \ --location="${REGION}" \ --role="roles/artifactregistry.writer" \ --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/github-prod-pool/attribute.repository/${GITHUB_REPO}" A pattern I like a lot on top of this: split identities by environment. A read-only terraform plan identity that PRs are allowed to use, and a separate apply / deploy identity restricted to the production GitHub Environment with required human reviewers. The hardened workflow On the GitHub side, this is what the deploy workflow looks like. Notice three things: least-privilege permissions, id-token: write only on the job that actually needs it, and every action pinned to a full 40-character commit SHA (this alone would have neutralized tj-actions): name: Build and Deploy to GKE on: push: branches: [ "main" ] # Global default: least privilege permissions: contents: read jobs: deploy: runs-on: ubuntu-latest environment: production permissions: contents: read id-token: write # ONLY on the job that exchanges OIDC tokens steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - run: npm ci --ignore-scripts - uses: google-github-actions/auth@6fc4af4b145ae7821d527454aa9bd537d1f2dc5f # v2.1.7 with: workload_identity_provider: "projects/123456789/locations/global/workloadIdentityPools/github-prod-pool/providers/github-oidc-provider" That --ignore-scripts is not decoration, by the way. More on that in a second. The demo: forging a PR token at the STS border Attack vector 3 builds an OIDC token that looks exactly like what Mini Shai-Hulud would bring to the border -- right repository, right owner ID, but: { "iss": "https://token.actions.githubusercontent.com", "repository": "acme-corp/payments-api", "ref": "refs/pull/99/merge", "event_name": "pull_request_target", "runner_environment": "self-hosted" } It then sends it straight to https://sts.googleapis.com/v1/token asking for a cloud-platform access token. STS rejects the exchange. Customs did its job. The repo also has a real GitHub workflow for this (.github/workflows/attack-sim-oidc-hijack.yml) that triggers on pull_request and tries to authenticate against the hardened provider. Open a PR, watch it fail at the GCP boundary. It is honestly one of the most satisfying red X's you will ever see in GitHub Actions. The demo: the worm that never gets to run Attack vector 1 is the ChainDrop / Shai-Hulud pattern in miniature. The demo package is about as small as it gets: { "name": "chaindrop-shai-hulud-simulator", "private": true, "scripts": { "preinstall": "node setup.mjs" } } And setup.mjs does what the real worms did first -- it scans process.env for anything that smells like a credential (safely, values are redacted in the simulation): const sensitiveVars = Object.keys(process.env).filter((k) => /GOOGLE|GCP|AWS|TOKEN|SECRET|KEY|GITHUB|KUBE/i.test(k) ); Run a plain npm install and the hook fires before a single dependency is even fetched. Run npm install --ignore-scripts (or drop ignore-scripts=true into your .npmrc) and it simply never executes. One flag. That is the difference between "CI runner leaked its environment" and "nothing happened". --- Pillar 3: Stop Sharing the Master Key on GKE So the pipeline is locked down. What about inside the cluster? The legacy setup on GKE is that every Pod on a node inherits the node's service account. And very often that is the Compute Engine default service account, ...-compute@developer.gserviceaccount.com, sometimes with roles/editor and the full cloud-platform scope. That is one giant master key on a lanyard, shared by every Pod on the node. One SSRF in one Pod, one call to the metadata server, and the attacker owns the whole project. Workload Identity Federation for GKE flips this. One Pod, one scoped identity. Each Kubernetes ServiceAccount gets its own tiny, least-privilege IAM badge, and the GKE Metadata Server hands out short-lived tokens for that identity only. And just like on the CI side, you no longer need an intermediate Google service account. Bind IAM directly to the Kubernetes ServiceAccount: kubectl create namespace payments-prod kubectl create serviceaccount payments-api-ksa --namespace payments-prod gcloud secrets add-iam-policy-binding "stripe-webhook-secret" \ --role="roles/secretmanager.secretAccessor" \ --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/payments-prod/sa/payments-api-ksa" Read that --member string once slowly. It says: exactly this ServiceAccount, in exactly this namespace, in exactly this cluster's identity pool, can read exactly this one secret. That is what least privilege looks like when you stop pretending. The workload itself is hardened too. From the demo repo: apiVersion: apps/v1 kind: Deployment metadata: name: payments-vault-api namespace: payments-prod spec: template: spec: serviceAccountName: payments-api-ksa securityContext: runAsNonRoot: true runAsUser: 65532 seccompProfile: type: RuntimeDefault containers: - name: api # Immutable digest, verified by Binary Authorization image: us-central1-docker.pkg.dev/.../payments-api@sha256:e882491c... securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] The namespace enforces the restricted Pod Security Standard, and the ServiceAccount sets automountServiceAccountToken: false. Non-root, read-only root filesystem, no capabilities, image by digest. The demo app (app/main.py) is a tiny Python service that asks the GKE Metadata Server who it is and how long its token lives, and reports whether GOOGLE_APPLICATION_CREDENTIALS exists in its environment. On a properly configured cluster, it tells you its scoped principal, a short TTL, and "static_json_keys_present": false. Which is exactly the answer you want. To verify on your own cluster, check Kubernetes Engine → Clusters → Security for Workload Identity: Enabled (<PROJECT_ID>.svc.id.goog), and make sure your node pools have the GKE Metadata Server enabled and are not running on the default Compute SA. --- Pillar 4: No Provenance? No Entry to GKE Here is the last gap. Even if your pipeline is perfect, what stops someone from pushing an image from their laptop -- with a stolen maintainer token, just like ChainDrop -- and deploying it? Three things, stacked: 1. Immutable tags in Artifact Registry Remember tj-actions? Mutable tags were the entire attack. Container tags have exactly the same problem: :v1.0.0 today might not be :v1.0.0 tomorrow. gcloud artifacts repositories create prod-containers \ --repository-format=docker \ --location="${REGION}" \ --immutable-tags \ --description="Production container registry with immutable tags" Once a tag is pushed, it is pinned. Attack vector 4 in the demo tries to repoint an existing v1.0.0 release to a poisoned image, and Artifact Registry refuses. And in your manifests, deploy by digest (@sha256:...) anyway. 2. SLSA Level 3 provenance from Cloud Build ChainDrop proved SLSA L2 is not enough when the attacker is upstream of the build. You want builds running on isolated, hermetic builders that you control -- not on a maintainer's laptop. Regional Cloud Build generates verifiable build provenance for you: gcloud builds submit \ --region="${REGION}" \ --tag="${REGION}-docker.pkg.dev/${PROJECT_ID}/prod-containers/payments-api:v1.0.0" . gcloud artifacts docker images describe \ "${REGION}-docker.pkg.dev/${PROJECT_ID}/prod-containers/payments-api:v1.0.0" \ --show-provenance In the console, open the image digest in Artifact Registry and look at the Build Provenance tab: builder identity, source commit, the whole chain. The Vulnerabilities tab gives you CVE scanning for free while you are there. 3. Binary Authorization as the GKE admission gate Provenance is useless if nothing checks it. Binary Authorization is the bouncer at the cluster door: defaultAdmissionRule: evaluationMode: REQUIRE_ATTESTATION enforcementMode: ENFORCED_BLOCK_AND_AUDIT_LOG Now try to sneak in a rogue Pod. The demo has one ready, appropriately named: apiVersion: v1 kind: Pod metadata: name: chaindrop-rogue-miner namespace: payments-prod spec: serviceAccountName: payments-api-ksa containers: - name: rogue-container image: docker.io/library/alpine:latest command: ["sh", "-c", "echo 'Attempting unauthorized workload execution...' && sleep 3600"] kubectl apply -f k8s/rogue-pod-attack.yaml Error from server (Forbidden): admission webhook "imagepolicywebhook.image-policy.k8s.io" denied the request: Image docker.io/library/alpine:latest denied by Binary Authorization default admission rule: No valid attestation found for attestor projects/your-gcp-project-id/attestors/prod-ci-attestor No stamp, no entry. It does not matter whose token was used to push it. --- The Cheat Sheet: Threat → Control Here is the whole talk in one table: | Threat / Incident | Root Vulnerability | CI/CD Fix | Google Cloud / GKE Fix | | ---------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- | | tj-actions / reviewdog (CVE-2025-30066) | Mutable action tags + runner memory secret dump | Pin actions to 40-char commit SHAs | Kill JSON keys (iam.disableServiceAccountKeyCreation) + short-lived WIF tokens + VPC-SC | | Nx s1ngularity & loose OIDC trust | pull_request_target + WIF checking only repository | Never check out untrusted PR code in privileged workflows | Strict WIF CEL condition (repository_owner_id, ref, job_workflow_ref) | | Shai-Hulud 2.0 & Mini Shai-Hulud | Rogue SHA1HULUD runners + pnpm cache / PR OIDC theft | Audit runners; permissions: contents: read | Require runner_environment == 'github-hosted' + ref == 'refs/heads/main' in CEL | | ChainDrop & GhostAction | Compromised maintainers + preinstall droppers | --ignore-scripts; OIDC Trusted Publishing for npm | Immutable tags + Cloud Build SLSA L3 + Binary Authorization + VPC-SC | | GKE Pod compromise / SSRF | Default Compute SA exposed via metadata server | Drop capabilities; network policies | Workload Identity Federation for GKE with direct per-KSA IAM bindings | --- A Reality Check (Because Nothing Is Perfect) Strict CEL conditions will break things. The first time someone tries a legitimate workflow_dispatch from a release branch, it will fail. That is the point, but you will need to model your real deployment paths in the condition (or in separate providers) instead of opening it back up to "just the repository". SHA pinning is a maintenance tax. Forty-character hashes do not update themselves. Use Dependabot or Renovate to bump pinned SHAs, otherwise you trade a supply chain risk for an "everything is two years old" risk. --ignore-scripts is not free. Some packages genuinely need install scripts (native modules, binary downloads). You will need an allowlist or a separate, isolated step for those. It is still a much better default than "run arbitrary code from 1,500 transitive dependencies with my cloud credentials in the environment". Hardware keys are a people problem. YubiKeys get lost, left at home, or forgotten in a laptop bag. Plan for backup keys and a revocation process before you enforce signed commits on the main branch. Short-lived tokens are short, not zero. A one-hour token dumped from runner memory is still a one-hour window. This is exactly why VPC Service Controls matter: a stolen token should not work from an attacker's workstation IP in the first place. Binary Authorization needs a break-glass plan. When prod is on fire at 3 AM and your attestor pipeline is down, you need a documented, audited bypass. Otherwise somebody will "temporarily" turn enforcement off and forget about it. --- Getting Started Everything above is in the demo repo, and you can run it from Cloud Shell with one command: git clone https://github.com/calganaygun/trust-no-pipeline-gcp-demo.git && \ cd trust-no-pipeline-gcp-demo && \ ./scripts/01-setup-gcp-guardrails.sh The bootstrap script detects your active project and sets up: 1. The required APIs (iam, sts, artifactregistry, cloudbuild, binaryauthorization, containerscanning) 2. A CI service account (ci-prod-deployer) and the iam.disableServiceAccountKeyCreation policy 3. A Workload Identity Pool (github-prod-pool) with a CEL-locked OIDC provider requiring repository_owner_id, refs/heads/main, and github-hosted runners 4. An Artifact Registry repo (prod-containers) with immutable tags, bound directly to the federated principal 5. A sample payments-api:v1.0.0 build through regional Cloud Build, with provenance Then launch the attack simulator and watch all five attacks fail: ./scripts/02-run-attack-vectors.sh | # | Attack | Blocked by | | - | --------------------------------------------------- | ------------------------------------------------ | | 1 | ChainDrop / Shai-Hulud preinstall worm | npm install --ignore-scripts | | 2 | Minting a backdoor Service Account JSON key | iam.disableServiceAccountKeyCreation | | 3 | PR / rogue runner OIDC token exchange | WIF CEL attribute condition at GCP STS | | 4 | tj-actions-style tag overwrite of :v1.0.0 | Artifact Registry immutable tags | | 5 | Unattested rogue Pod on GKE | Binary Authorization admission | If you want to click through the slides too, they are a single index.html -- run python3 -m http.server 8080 in Cloud Shell and use Web Preview, or just grab them at calgan.dev. --- The Bottom Line If you take nothing else from this post, take the four stickers from the last slide back to your desk on Monday morning: 1. Sign and pin. Hardware-backed GPG commits, and 40-character SHAs for every action. 2. Burn your JSON keys. iam.disableServiceAccountKeyCreation, today. 3. Check the branch and the runner. WIF CEL: main + github-hosted, not just the repo name. 4. Stamp your artifacts. SLSA L3 provenance in, Binary Authorization at the door. Attackers have figured out that your pipeline is the softest, most privileged thing you own. It has production credentials, it runs untrusted code all day long, and nobody audits it like they audit prod. The era of "it is our CI, it is fine" is over. The era of treating the delivery truck with the same suspicion as anything else coming through the gate is here. Trust no pipeline. Not even your own. --- References All the sourcing for this post: - Trust No Pipeline -- slides -- the interactive deck from the talk - trust-no-pipeline-gcp-demo -- the live attack/defense demo repo used throughout this post - CISA alert on tj-actions/changed-files (CVE-2025-30066) -- the official advisory - NVD: CVE-2025-30066 -- the CVE record - Nx s1ngularity GitHub security advisory -- the pull_request_target compromise of nx - Wiz Research: Shai-Hulud supply chain worm -- breakdown of the npm worm waves - Panther Labs: Shai-Hulud analysis -- detection-focused write-up - Aikido: credential-stealing npm worm -- package-level analysis - 2026 npm worm defense analysis -- Mini Shai-Hulud and defenses - Elastic Security Labs: Shai-Hulud & ChainDrop -- ChainDrop deep dive - Palo Alto Unit 42: ChainDrop npm worm -- Bun dropper and Ethereum C2 analysis - StepSecurity: ChainDrop incident breakdown -- CI/CD-centric timeline - GitGuardian: GhostAction, 3,000+ secrets stolen -- rogue workflow secret exfiltration - Google Cloud: Workload Identity Federation with deployment pipelines -- the official WIF + GitHub Actions guide - Google Cloud: Workload Identity Federation for GKE -- per-KSA identities on GKE - Google Cloud: Binary Authorization -- admission-time attestation enforcement

  2. More context: nrwl/new-cloud-bench + some numbers where the baseline runs on Blacksmith (TLDR: much slower and more compute than Nx. Cloud).

    150 views
  3. CVE-2026-119282: CVE-2026-119282: OS Command Injection via Plugin Options in Nx (@nx/maven and @nx/gradle) CVE-2026-119282: OS Command Injection via Plugin Options in Nx (@nx/maven and @nx/gradle) > Vulnerability ID: CVE-2026-119282 > CVSS Score: 7.3 > Published: 2026-10-30 An OS command injection and argument injection vulnerability exists in the Nx build system and its official Maven (@nx/maven) and Gradle (@nx/gradle) plugins. When constructing the project dependency graph, the plugins invoke underlying Java build wrappers via subshells while interpolating unsanitized plugin options directly from nx.json into the command arguments. An attacker who supplies a crafted nx.json file via a repository or pull request can execute arbitrary shell commands with the privileges of the user or CI/CD runner invoking any Nx command that computes the project graph. TL;DR Nx versions prior to 22.7.9 and 23.1.2 pass unvalidated configuration options from nx.json into child process executions with shell: true in @nx/maven and @nx/gradle. Running Nx commands in an untrusted repository or pull request allows arbitrary OS command execution. --- ⚠️ Exploit Status: POC Technical Details - Vulnerability Class: OS Command Injection (CWE-78) / Argument Injection (CWE-88) - CVSS v4.0 Score: 7.3 HIGH (CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) - Attack Vector: Local via untrusted nx.json in repository or pull request - Affected Components: nx, @nx/maven, @nx/gradle (>= 21.0.0, < 22.7.9 and >= 23.0.0, < 23.1.2) - Patched Versions: 22.7.9 and 23.1.2 - EPSS Score: 0.14% (3.38th percentile) - CISA KEV Status: Not Listed Affected Systems - nx - @nx/maven - @nx/gradle - nx: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - nx: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) - @nx/maven: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - @nx/maven: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) - @nx/gradle: >= 21.0.0, < 22.7.9 (Fixed in: 22.7.9) - @nx/gradle: >= 23.0.0, < 23.1.2 (Fixed in: 23.1.2) Code Analysis Commit: 02c5d59 Introduce safeSpawn and safeExecFileSync utilities and replace shell: true invocations in @nx/maven and @nx/gradle. Commit: 60196b7 Backport safeSpawn and plugin analyzer security fixes for CVE-2026-119282. Commit: c227f47 Release commit containing fixes for @nx/maven and @nx/gradle command injection. Mitigation Strategies - Upgrade both the core nx package and any installed @nx/maven or @nx/gradle plugins to matched release versions 22.7.9 or 23.1.2. - Enforce strict code review requirements on nx.json using repository CODEOWNERS rules, treating it with the same sensitivity as CI workflow definitions and build scripts. - Isolate CI/CD execution on untrusted pull requests so that nx commands do not run with access to production secrets or write permissions before human review. Remediation Steps: 1. Identify whether @nx/maven or @nx/gradle is installed in package.json or configured in nx.json. 2. Run 'nx migrate 22.7.9' (for 22.x workspaces) or 'nx migrate 23.1.2' (for 23.x workspaces) to update package.json dependencies. 3. Install the updated packages using your package manager (npm install, yarn, or pnpm install). 4. Verify that node_modules/nx, node_modules/@nx/devkit, node_modules/@nx/maven, and node_modules/@nx/gradle all resolve to version 22.7.9 or 23.1.2+. References - GitHub Security Advisory GHSA-qm8w-vx34-5j9j - NVD - CVE-2026-119282 Detail - CVE.org Record: CVE-2026-119282 - CVEListV5 Official JSON Record for CVE-2026-119282 - GitHub Pull Request #36626 - Fix Command Injection in Maven and Gradle Plugins - Fix Commit 02c5d59e8dbbbe30963c0181c6a22cd8c1190a25 - Fix Commit 60196b7cfcffcada35b6b90e53bf1a78d049b548 - Fix Commit c227f4780b054c512965c73aa480a48f9f781409 - Nx Release 22.7.9 - Nx Release 23.1.2 --- Read the full report for CVE-2026-119282 on our website for more details including interactive diagrams and full exploit analysis.

  4. The Nx Malware Ran Claude Code With --dangerously-skip-permissions. Could It on Yours? - Book: Prompt Engineering Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- At 22:32 UTC on August 26, 2025, nx 21.5.0 went up on npm with a postinstall script called telemetry.js. Inside it, Snyk found a small lookup table: claude: { cmd: 'claude', args: ['--dangerously-skip-permissions', '-p', PROMPT] }, gemini: { cmd: 'gemini', args: ['--yolo', '-p', PROMPT] }, q: { cmd: 'q', args: ['chat', '--trust-all-tools', '--no-interactive', PROMPT] } Three coding agents, each with its approval prompts switched off, each handed a prompt asking it to find files that hold secrets. Seven more malicious versions followed over the next two hours. npm pulled them at 02:44 UTC, per StepSecurity's timeline. GitGuardian later counted 2,349 distinct secrets in the public repositories the payload created. The flags are the part worth a second look, because the attacker picked them, not the developer. What telemetry.js did, step by step The Nx security advisory traces the entry point to a GitHub Actions workflow that echoed a pull request title inside a run: step, under the pull_request_target trigger. A crafted title ran shell commands with a read/write token. The Nx team believes that is how the attacker got the npm publishing token. From the StepSecurity write-up, the payload then ran in this order: 1. Exit immediately on Windows. Linux and macOS only. 2. Collect what it could reach without any AI: a GitHub token via gh auth token, npm tokens from .npmrc, plus environment variables, hostname and OS details (SafeDep). 3. Check which of claude, gemini and q were on the PATH, and run each one found with the flags above. 4. The prompt told the agent to search $HOME, $HOME/.config, /etc, /var and /tmp to a depth of 8, without sudo, for names like .env, id_rsa and wallet files, and write matching paths to /tmp/inventory.txt. 5. Read every listed file, triple base64-encode the lot, and push it as results.b64 to a new public repo named s1ngularity-repository in the victim's own GitHub account. 6. Append sudo shutdown -h 0 to ~/.bashrc and ~/.zshrc. Step 2 matters for everything below. The script already ran as you, with your files and your tokens. The agents gave it no access it lacked. They gave it a search tool that adapts to whatever your disk looks like. The damage kept going after npm pulled the versions. On August 28 a second wave used the stolen GitHub tokens to flip private repositories to public. GitGuardian counted 10,767 of them. Four prompts, uneven results Okta's threat intelligence team noted that successive package versions carried at least four different prompts, as the attacker iterated. Paul McCarty at Safety reran all four on a sandboxed Mac with the same flags. Run counts vary and are often unstated: 10 runs for Claude on the third version, 5 for Gemini on the fourth, one described run for Claude on the second: | Prompt version | Gemini CLI (--yolo) | Claude Code (--dangerously-skip-permissions) | |---|---|---| | 1: wallets and keys, stated openly | 100% | Refused | | 2: "authorized penetration tester" | 50% | 100% | | 3: generic "file-search agent" | 0% | 20% | | 4: source-code exclusions | 50% | 50% | Claude refused the first version outright, quoted in the Safety post as "I cannot help with searching for and cataloging wallet-related files, private keys". Reframe the same job as a pentest and it complied on the run Safety describes. Several of the Gemini failures were sandbox or write limits, not refusals on content. It's a small sample, but with the flags held constant, wording alone swung the outcome from refusal to compliance. The field numbers agree that the AI step was the shaky part. Of the compromised systems GitGuardian studied, 33% had at least one LLM client installed. LLM tools were targeted on 366 systems, and only 95 of them wrote /tmp/inventory.txt. The new part is that an install script can hand the hunting to a model, and the model's refusals depend on phrasing an attacker can retry. Why the bypass warning never fired Claude Code's permission modes docs are clear that bypassPermissions belongs in containers or VMs. They also explain why that warning did nothing here. The first time you start an interactive session in that mode, Claude Code shows a dialog asking you to accept responsibility. In non-interactive mode, the -p the payload used, "no dialog is shown". Claude Code refuses bypass mode as root or under sudo, but the payload ran as your normal user and its own prompt said not to use sudo. By default, any process that can run claude can choose its mode, and gemini and q work the same way. Your own ~/.claude/settings.json is no barrier either: the permissions docs let you lock yourself out of bypass mode there, but a script running as you can edit that file. What a script running as you can't edit without a password is a file in a root-owned system directory. That is where the useful setting goes. Check your machine in five minutes First, the same check the payload ran, plus a look for settings that already skip prompts: for c in claude gemini q; do command -v "$c" >/dev/null && echo "on PATH: $c" done grep -nE 'bypassPermissions|skipDangerousMode' \ ~/.claude/settings.json 2>/dev/null A "defaultMode": "bypassPermissions" in your user settings means a script doesn't even need the flag. skipDangerousModePermissionPrompt only silences the interactive dialog, which -p never shows anyway. Claude Code. The permissions docs say permissions.disableBypassPermissionsMode set to "disable" prevents bypass mode, and that it is most useful in managed settings. Per the managed settings docs, the file lives at /Library/Application Support/ClaudeCode/managed-settings.json on macOS and /etc/claude-code/managed-settings.json on Linux, and no user, project or --settings value overrides it. Check for an existing file with cat first, because this overwrites it: # Linux: D=/etc/claude-code D="/Library/Application Support/ClaudeCode" sudo mkdir -p "$D" sudo tee "$D/managed-settings.json" >/dev/null <<'EOF' { "permissions": { "disableBypassPermissionsMode": "disable" } } EOF Two caveats from the same page. On WSL, /etc/claude-code is user-writable, so a file there alone protects nothing. And if the operating system denies the read, sessions start without the policy; /status and claude doctor record that failure. Gemini CLI. The enterprise guide documents security.disableYoloMode: true, which forces every tool call to ask, in the system settings file at /Library/Application Support/GeminiCli/settings.json or /etc/gemini-cli/settings.json. The same guide says the path can be overridden with the GEMINI_CLI_SYSTEM_SETTINGS_PATH environment variable, and whoever launches gemini controls its environment. Treat it as a guard against accidents, not against a hostile script. Gemini CLI itself was replaced by Antigravity CLI for unpaid and Google One users on June 18, 2026, per the same page. Amazon Q. AWS says the Q Developer CLI has been rebranded to Kiro. Check what an old q binary on your PATH still does before trusting it. The install step. None of the above stops step 2. The way in was a postinstall script, and npm's ignore-scripts config means "npm does not run scripts specified in package.json files". npm config set ignore-scripts true turns that on for your user. Some packages need their install step, and you then run those deliberately. Where Hermes fits, and where it doesn't I build Hermes IDE, so weigh this section with that in mind. Hermes would not have stopped telemetry.js. It isn't a sandbox, and a postinstall script doesn't go through it. The settings above are what refuse a bypass launch. What the incident does show is why Hermes has one fixed rule: it never answers a permission prompt for you and never types into a terminal on its own (README). The payload worked by removing the human from the approval loop. A tool that sits between you and your agents and clicks yes on your behalf would rebuild that hole on purpose, as a feature. I made the case at more length in No Script Should Ever Click 'Allow' for Your Coding Agent. !The Hermes New task sheet with Claude Code, the orders-api project, a new hermes/ worktree, Accept edits, and the Hermes will run preview of the claude command The ⌘N New task sheet: Claude Code, the orders-api project, a new hermes/ worktree, Accept edits, model and effort chips, and the "Hermes will run" preview of the claude command. When you start a task with ⌘N (Ctrl+Shift+N on Windows and Linux), you pick the agent, model and effort for it, and the agent runs its own interface in a real terminal, so its permission prompts reach you unchanged. When an agent does ask, the attention inbox counts it, ⌘I (Ctrl+Shift+I on Windows and Linux) jumps to the one that has waited longest, and you answer in that agent's own terminal. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. Download it here, and the code is on GitHub. It is not affiliated with Nous Research, Anthropic, OpenAI, Google, AWS or any other agent vendor. --- If this was useful The most telling detail in this incident is the Safety table: the same agent refused a request and then carried it out once the wording changed. That is prompt engineering, used against you. My Prompt Engineering Pocket Guide covers how framing and instructions shape what a model does, prompt injection included, so you can read a prompt like the one in telemetry.js and see what each line is doing. ![Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs](amazon.com)

  5. YouTube · · YouTube description · githubrepo_cloud · nrwl/nx

    ⚡ [Top 5 AI Repos This Week] nx: The Free Tool Taking Over GitHub! #shorts Discover nrwl/nx: The Monorepo Platform that amplifies both developers and AI agents. Nx optimizes your builds, scales your CI, and fixes failed PRs automatically. Ship in half the time. 💬 Comment "REPO" below for the direct link, or grab it from the links below: 👉 Interactive Star History & Live Analysis: githubrepo.cloud ⭐ Embed Live Verified Badges in your README: githubrepo.cloud 🔗 Direct GitHub Repository: nrwl/nx 📊 Repository Specs: ⭐ Stars: 29,393 🍴 Forks: 3,008 💻 Language: TypeScript 📜 License: MIT #shorts #angular #build #buildsystem #buildtool #musicproducer #audioengineer #stems #musicproduction

    2 likes · 131 views
  6. DEV · · @vzn-vx · nrwl/nx

    Say which projects may depend on which Tag your projects by layer and say which layers may depend on which. vx lock refuses a dependency that breaks a rule. Here UI may not depend on a utility, and packages/c (UI) depends on packages/a: !diagram // .vx/workspace.ts import { defineWorkspace } from '@vzn/vx/config' export default defineWorkspace({ run: { rules: { dependencies: [{ from: 'tag:type:ui', deny: ['tag:type:util'] }], }, }, }) $ vx lock ✗ vx lock lock not written 1 dependency breaks run.rules.dependencies packages/c depends on packages/a, which run.rules.dependencies[0] (from 'tag:type:ui') denies fix drop the dependency, or change the project's tags or the rule $ echo $? 1 The edge here is a task edge (c#build depends on a#build), not a package.json dependency. Both count. <small>Demo workspace · vx 69617eb · 2026-10-23</small> Tags and rules A project names its tags in its own config: // packages/button/vx.config.ts import { defineProject } from '@vzn/vx/config' export default defineProject({ tags: ['type:ui'], tasks: { build: { exec: { command: 'tsc -b' } } }, }) The workspace says what each tag may depend on. A selector is tag:<glob> or a project-name glob: // .vx/workspace.ts import { defineWorkspace } from '@vzn/vx/config' export default defineWorkspace({ run: { rules: { dependencies: [ { from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] }, { from: 'tag:type:ui', deny: ['tag:type:feature', 'tag:type:app'] }, ], }, }, }) A dependency must match one of allow (if given) and none of deny. A deny beats any allow. A project no rule selects is free. vx lock checks them, runs don't vx lock checks each project's direct dependencies: package.json and cross-project dependsOn. It names every broken edge and writes nothing. Fix the edge and the lock is written: $ vx lock ✓ vx lock 6 project configs → .vx/projects.lock.json $ vx lock --check ✓ vx lock --check up to date (6 projects) vx lock --check in CI fails the same way, so a pull request with a forbidden dependency goes red. Runs read the lock, which already passed the rules, so they pay nothing (vx lock). From Nx nx() from @vzn/vx-migrate reads the depConstraints of @nx/enforce-module-boundaries from your ESLint config and fills run.rules.dependencies. It reads the file without running it, so ESLint never loads on a vx run. // eslint.config.mjs (Nx) depConstraints: [ { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui'] }, { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:feature'] }, ] becomes: dependencies: [ { from: 'tag:type:app', allow: ['tag:type:feature', 'tag:type:ui'] }, { from: 'tag:type:ui', deny: ['tag:type:feature'] }, ] One difference: Nx's lint rule checks source imports. vx checks declared dependencies. allSourceTags, /regex/ tags and external-import bans are left out. The vx-migrate README lists them. The schema is in the configuration reference. More at vznjs.github.io. --- Originally published on the vx blog. vx is open-source (MIT) task orchestration with a build cache: vznjs.github.io · GitHub. <sub>_Written with AI assistance._</sub>

  7. @juristr @victorsavkin @brandontroberts Sure! It has been 6 years(!!) at this point, but if I remember correctly there was a decision back around NX 11 that removed support for configuration composition a la angular cli, due to issues relating to task output caching nx#2839

    11 views

Achievements

Awards earned by the repositories nrwl owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.