diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73de4c3..efc8dcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,3 +48,19 @@ jobs: - name: Test run: npm run test:all + + - name: Install action dependencies + working-directory: packages/action + run: npm ci --legacy-peer-deps + + - name: Build action + working-directory: packages/action + run: npm run build + + - name: Generate marketplace action + working-directory: packages/action + run: npm run generate:marketplace + + - name: Test action + working-directory: packages/action + run: npm test diff --git a/.gitignore b/.gitignore index 872d5f6..30c1337 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,9 @@ out dist .output +# Generated GitHub Marketplace action distribution +marketplace/obfuscan-action/ + # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js diff --git a/README.md b/README.md index 296d614..69873fa 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,45 @@ pnpm add @obfuscan/core @obfuscan/rules The `core` package ships the engine; `rules` ships language configs and tree-sitter query assets, not parser grammars. Hosts that want parser-backed custom detectors provide their own grammars via `RuleSet.loadGrammar()` / `GrammarHandle.parse()`. We use SemVer for the engine and CalVer (`2026.04.0`) for the rules. +## GitHub Action + +Run obfuscan directly in pull request CI and have it annotate findings, write a job summary, and upsert a Markdown PR comment. + +```yaml +name: obfuscan + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + issues: write + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: ByteBardOrg/obfuscan-action@v1 + with: + fail-on: block +``` + +Useful inputs: + +- `fail-on`: `block` (default), `warn`, or `never`. +- `min-severity`: `info` (default), `warn`, or `block`. +- `comment`: `true` (default) or `false`. +- `max-findings`: maximum findings shown in the Markdown report, default `50`. +- `disabled-detectors`: comma or newline separated detector ids. +- `allowlist-path`: defaults to `.obfuscan/allowlist.json`. + +The Marketplace action is distributed from `ByteBardOrg/obfuscan-action`. Generate that repo's contents locally with `cd packages/action && npm run marketplace`; the generated `marketplace/obfuscan-action` folder is ignored in this repo. + ## Using `@obfuscan/rules` `@obfuscan/core` loads language configs from `@obfuscan/rules` by default, so normal usage is just installing both packages. @@ -205,7 +244,7 @@ Pre-1.0. The detector framework, scoring, suppression, and tier-1/tier-2 languag - [x] Manifest detectors for npm, PyPI, GitHub Actions, Dockerfile - [x] Tier-2 language rules (Go, Rust, C#, Java, Kotlin, Lua, Perl, VBScript) - [ ] `@obfuscan/cli` 1.0 with SARIF output -- [ ] `@obfuscan/github-action` +- [x] `@obfuscan/github-action` v1 - [ ] `@obfuscan/llm-verify` optional Layer-D package - [ ] Reproducible benchmark suite against [Datadog malicious-software-packages-dataset](https://github.com/DataDog/malicious-software-packages-dataset) diff --git a/docs/architecture.md b/docs/architecture.md index 2edfde1..16bbeef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -260,8 +260,9 @@ beyond `fileResolver`. These belong to separate packages so the core stays small and embeddable: - **CLI** — `@obfuscan/cli` (separate package, not in this scaffold). -- **GitHub Action** — `obfuscan/obfuscan-action` (separate repo, not in - this scaffold). +- **GitHub Action** — source lives in `packages/action`; Marketplace distribution + is generated into ignored local output `marketplace/obfuscan-action` and released + from the dedicated `ByteBardOrg/obfuscan-action` repository as `@v1`. - **Default rule pack** — `@obfuscan/rules`. CalVer, releases more often than core. Resolved at runtime from a workspace sibling, an installed `node_modules/@obfuscan/rules`, or `OBFUSCAN_RULES_DIR`. diff --git a/packages/action/package-lock.json b/packages/action/package-lock.json new file mode 100644 index 0000000..fc698cd --- /dev/null +++ b/packages/action/package-lock.json @@ -0,0 +1,1941 @@ +{ + "name": "@obfuscan/github-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@obfuscan/github-action", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@obfuscan/core": "file:../core", + "@obfuscan/rules": "file:../rules" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.21.0", + "typescript": "^5.4.0", + "vitest": "^1.5.0" + }, + "engines": { + "node": ">=20" + } + }, + "../core": { + "name": "@obfuscan/core", + "version": "0.2.0", + "license": "Apache-2.0", + "dependencies": { + "fast-levenshtein": "^3.0.0", + "minimatch": "^9.0.0", + "parse-diff": "^0.11.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.4.0", + "vitest": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@obfuscan/rules": "*" + }, + "peerDependenciesMeta": { + "@obfuscan/rules": { + "optional": false + } + } + }, + "../rules": { + "name": "@obfuscan/rules", + "version": "2026.04.0", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@obfuscan/core": { + "resolved": "../core", + "link": true + }, + "node_modules/@obfuscan/rules": { + "resolved": "../rules", + "link": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/pkg-types/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/action/package.json b/packages/action/package.json new file mode 100644 index 0000000..68598ab --- /dev/null +++ b/packages/action/package.json @@ -0,0 +1,29 @@ +{ + "name": "@obfuscan/github-action", + "version": "1.0.0", + "private": true, + "description": "GitHub Action for obfuscan pull request scanning.", + "license": "Apache-2.0", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prepare-dist": "node ./scripts/prepare-dist.mjs", + "build": "npm run prepare-dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js", + "generate:marketplace": "node ./scripts/generate-marketplace.mjs", + "marketplace": "npm run build && npm run generate:marketplace", + "test": "vitest run --config test/vitest.config.ts" + }, + "dependencies": { + "@obfuscan/core": "file:../core", + "@obfuscan/rules": "file:../rules" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "esbuild": "^0.21.0", + "typescript": "^5.4.0", + "vitest": "^1.5.0" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/action/scripts/generate-marketplace.mjs b/packages/action/scripts/generate-marketplace.mjs new file mode 100644 index 0000000..64572da --- /dev/null +++ b/packages/action/scripts/generate-marketplace.mjs @@ -0,0 +1,418 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); +const actionDir = path.join(root, "packages", "action"); +const sourceDist = path.join(actionDir, "dist"); +const target = path.join(root, "marketplace", "obfuscan-action"); + +await assertExists(path.join(sourceDist, "index.js"), "Run `npm run build` in packages/action before generating marketplace output."); + +await fs.rm(target, { recursive: true, force: true }); +await fs.mkdir(path.join(target, ".github", "workflows"), { recursive: true }); +await fs.cp(sourceDist, path.join(target, "dist"), { recursive: true }); +await fs.copyFile(path.join(root, "LICENSE"), path.join(target, "LICENSE")); + +await write("action.yml", actionYml()); +await write("README.md", readme()); +await write("GENERATED.md", generated()); +await write("SECURITY.md", security()); +await write("CONTRIBUTING.md", contributing()); +await write("CODE_OF_CONDUCT.md", codeOfConduct()); +await write(".gitignore", gitignore()); +await write(path.join(".github", "workflows", "smoke.yml"), smokeWorkflow()); + +console.log(`Generated ${path.relative(root, target)}`); + +async function assertExists(file, message) { + try { + await fs.access(file); + } catch { + throw new Error(message); + } +} + +async function write(relativePath, content) { + await fs.writeFile(path.join(target, relativePath), content.trimStart() + "\n", "utf8"); +} + +function actionYml() { + return ` +name: obfuscan +description: Detect obfuscated code and likely backdoors in pull-request diffs. +author: ByteBardOrg + +inputs: + github-token: + description: GitHub token used to read diffs and write pull request comments. + required: false + default: \${{ github.token }} + fail-on: + description: Severity that fails the workflow. One of block, warn, never. + required: false + default: block + min-severity: + description: Minimum severity to report. One of info, warn, block. + required: false + default: info + comment: + description: Whether to create or update a pull request comment. + required: false + default: "true" + max-findings: + description: Maximum number of findings shown in the Markdown report. + required: false + default: "50" + allowlist-path: + description: Workspace-relative allowlist path. + required: false + default: .obfuscan/allowlist.json + disabled-detectors: + description: Comma or newline separated detector ids to disable. + required: false + default: "" + file-timeout-ms: + description: Optional per-file detector timeout in milliseconds. + required: false + default: "" + +outputs: + findings-total: + description: Total number of findings. + findings-block: + description: Number of block findings. + findings-warn: + description: Number of warn findings. + findings-info: + description: Number of info findings. + conclusion: + description: pass or fail. + +runs: + using: node20 + main: dist/index.js + +branding: + icon: shield + color: purple +`; +} + +function readme() { + return ` +# obfuscan action + +Detect obfuscated code and likely backdoors in pull-request diffs. Multi-language. Diff-aware. Pure offline. Built for GitHub code review. + +## Quick start + +\`\`\`yaml +name: obfuscan + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + issues: write + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: \${{ github.event.pull_request.head.sha }} + + - uses: ByteBardOrg/obfuscan-action@v1 + with: + fail-on: block +\`\`\` + +The action scans the PR diff, annotates findings, writes a job summary, and upserts one Markdown PR comment. + +## What it catches + +obfuscan looks for the patterns common to supply-chain attacks: + +- Decode-then-execute chains like \`eval(Buffer.from(payload, "base64").toString())\`. +- Dynamic execution of non-literal code across JavaScript, Python, PowerShell, Bash, PHP, Ruby, Go, Rust, C#, Java, Kotlin, Lua, Perl, and VBScript. +- Suspicious install-time behavior in \`package.json\`, \`setup.py\`, \`build.rs\`, GitHub Actions workflows, and Dockerfiles. +- Obfuscation signals including high-entropy strings, encoded string arrays, bidi controls, homoglyph identifiers, and very long generated lines. + +Static analysis is a reviewer aid, not a proof of safety. Treat findings as high-signal review prompts. + +## Distribution model + +GitHub runs JavaScript actions directly from the checked-out action repository and does not run \`npm install\`. This repo therefore contains generated runtime artifacts under \`dist/\`, including a bundled copy of the exact \`@obfuscan/rules\` JSON files used by the scanner. + +Do not edit \`dist/\` or \`dist/rules\` by hand. They are generated from the main obfuscan repository: + +\`\`\`bash +cd packages/action +npm run marketplace +\`\`\` + +Source of truth: + +- Action source: +- Rules source: + +## Inputs + +| Input | Default | Description | +|---|---|---| +| \`github-token\` | \`\${{ github.token }}\` | Token used to read diffs and write PR comments. | +| \`fail-on\` | \`block\` | Fails the workflow at \`block\`, \`warn\`, or \`never\`. | +| \`min-severity\` | \`info\` | Minimum severity to report: \`info\`, \`warn\`, or \`block\`. | +| \`comment\` | \`true\` | Create or update a PR comment. Set to \`false\` for annotations and summaries only. | +| \`max-findings\` | \`50\` | Maximum findings shown in the Markdown report. | +| \`allowlist-path\` | \`.obfuscan/allowlist.json\` | Workspace-relative allowlist path. | +| \`disabled-detectors\` | empty | Comma or newline separated detector ids to disable. | +| \`file-timeout-ms\` | engine default | Optional per-file detector timeout in milliseconds. | + +## Outputs + +| Output | Description | +|---|---| +| \`findings-total\` | Total number of findings. | +| \`findings-block\` | Number of block findings. | +| \`findings-warn\` | Number of warn findings. | +| \`findings-info\` | Number of info findings. | +| \`conclusion\` | \`pass\` or \`fail\`. | + +## Examples + +Fail on warnings too: + +\`\`\`yaml +- uses: ByteBardOrg/obfuscan-action@v1 + with: + fail-on: warn +\`\`\` + +Only report blocking findings: + +\`\`\`yaml +- uses: ByteBardOrg/obfuscan-action@v1 + with: + min-severity: block +\`\`\` + +Run without PR comments: + +\`\`\`yaml +permissions: + contents: read + pull-requests: read + +steps: + - uses: actions/checkout@v4 + with: + ref: \${{ github.event.pull_request.head.sha }} + - uses: ByteBardOrg/obfuscan-action@v1 + with: + comment: false +\`\`\` + +Disable a detector: + +\`\`\`yaml +- uses: ByteBardOrg/obfuscan-action@v1 + with: + disabled-detectors: | + obf.high-entropy-literal + obf.long-line +\`\`\` + +## Permissions + +Use these permissions when \`comment: true\`: + +\`\`\`yaml +permissions: + contents: read + pull-requests: read + issues: write +\`\`\` + +Use these permissions when \`comment: false\`: + +\`\`\`yaml +permissions: + contents: read + pull-requests: read +\`\`\` + +GitHub may restrict \`GITHUB_TOKEN\` on pull requests from forks. In that case, obfuscan still emits annotations and a job summary, but PR comment creation can be skipped or fail with a warning. + +## Suppressions + +Suppress a known false positive inline: + +\`\`\`js +// obfuscan-disable-next-line obf.high-entropy-literal +const fixture = "U29tZSBsb25nIGRldGVjdG9yIGZpeHR1cmU="; +\`\`\` + +Or use \`.obfuscan/allowlist.json\`: + +\`\`\`json +{ + "paths": [ + { "pattern": "vendor/**", "maxSeverity": "warn", "reason": "third-party bundle" } + ] +} +\`\`\` + +## Releases + +This repo should maintain a moving major tag: + +\`\`\`bash +git tag v1 +git push origin v1 +\`\`\` + +For future \`1.x\` releases, move the major tag after publishing the release tag: + +\`\`\`bash +git tag v1.0.1 +git push origin v1.0.1 +git tag -f v1 v1.0.1 +git push -f origin v1 +\`\`\` + +## License + +Apache-2.0. See [LICENSE](./LICENSE). +`; +} + +function generated() { + return ` +# Generated distribution + +This folder is a generated GitHub Marketplace distribution for the obfuscan action. + +Do not maintain copied rules or bundled JavaScript by hand. Regenerate everything from the main obfuscan repository: + +\`\`\`bash +cd packages/action +npm run marketplace +\`\`\` + +Source of truth: + +- \`packages/action/src\` for action behavior. +- \`packages/rules/languages\` for rule JSON. +- \`packages/core/src\` for scanner behavior. + +GitHub Actions does not run \`npm install\` for JavaScript actions, so \`dist/\` intentionally contains the bundled runtime files needed by Marketplace users. +`; +} + +function security() { + return ` +# Security policy + +This repository distributes the GitHub Marketplace action for obfuscan. + +Report vulnerabilities in the scanner or action privately through GitHub Security Advisories in the source repository: + + + +Use \`security@bytebard.org\` as a fallback. + +Detection bypasses and false positives are usually public issues, not vulnerabilities. Open them in the source repository so rules and fixtures can be updated: + + +`; +} + +function contributing() { + return ` +# Contributing + +The source for this action lives in the main obfuscan repository: + + + +Open code changes, rule updates, false positives, and bypass reports there. This Marketplace repository is intended to hold generated distribution files: \`action.yml\`, \`dist/index.js\`, and bundled rule files. + +Useful local checks in the source repository: + +\`\`\`bash +cd packages/action +npm ci --legacy-peer-deps +npm run marketplace +npm test +\`\`\` +`; +} + +function codeOfConduct() { + return ` +# Code of Conduct + +This project follows the [Contributor Covenant 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). + +In short: be kind, assume good faith, criticize work and not people, and remember that the security community is small and reputations are forever. + +Report violations privately to \`conduct@bytebard.org\`. Reports are read by maintainers only. +`; +} + +function gitignore() { + return ` +# Logs and local installs +*.log +node_modules/ + +# Local editor and OS files +.DS_Store +.idea/ +.vscode/ + +# Keep the generated action distribution committed for GitHub Marketplace. +!dist/ +!dist/** +`; +} + +function smokeWorkflow() { + return ` +name: Smoke + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + +jobs: + smoke: + name: Action smoke test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + ref: \${{ github.event.pull_request.head.sha || github.sha }} + + - name: Run obfuscan action + uses: ./ + with: + github-token: \${{ github.token }} + comment: false + fail-on: never +`; +} diff --git a/packages/action/scripts/prepare-dist.mjs b/packages/action/scripts/prepare-dist.mjs new file mode 100644 index 0000000..baf229b --- /dev/null +++ b/packages/action/scripts/prepare-dist.mjs @@ -0,0 +1,32 @@ +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); +const actionDir = path.join(root, "packages", "action"); +const distDir = path.join(actionDir, "dist"); +const rulesDir = path.join(root, "packages", "rules"); + +const actionPkg = JSON.parse(await fs.readFile(path.join(actionDir, "package.json"), "utf8")); +const corePkg = JSON.parse(await fs.readFile(path.join(root, "packages", "core", "package.json"), "utf8")); +const rulesPkg = JSON.parse(await fs.readFile(path.join(rulesDir, "package.json"), "utf8")); + +await fs.rm(distDir, { recursive: true, force: true }); +await fs.mkdir(path.join(distDir, "rules"), { recursive: true }); +await fs.cp(path.join(rulesDir, "languages"), path.join(distDir, "rules", "languages"), { recursive: true }); +await fs.writeFile( + path.join(distDir, "rules", "package.json"), + JSON.stringify({ name: rulesPkg.name, version: rulesPkg.version }, null, 2) + "\n", + "utf8", +); + +await fs.writeFile( + path.join(actionDir, "src", "generated.ts"), + [ + `export const ACTION_VERSION = ${JSON.stringify(actionPkg.version)};`, + `export const BUNDLED_ENGINE_VERSION = ${JSON.stringify(corePkg.version)};`, + `export const BUNDLED_RULES_VERSION = ${JSON.stringify(rulesPkg.version)};`, + "", + ].join("\n"), + "utf8", +); diff --git a/packages/action/src/generated.ts b/packages/action/src/generated.ts new file mode 100644 index 0000000..276e483 --- /dev/null +++ b/packages/action/src/generated.ts @@ -0,0 +1,3 @@ +export const ACTION_VERSION = "1.0.0"; +export const BUNDLED_ENGINE_VERSION = "0.2.0"; +export const BUNDLED_RULES_VERSION = "2026.04.0"; diff --git a/packages/action/src/index.ts b/packages/action/src/index.ts new file mode 100644 index 0000000..304e0ca --- /dev/null +++ b/packages/action/src/index.ts @@ -0,0 +1,314 @@ +import { execFile } from "node:child_process"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; +import { scan, type Allowlist, type Finding, type ScanResult } from "@obfuscan/core"; +import { readInputs } from "./inputs.js"; +import { BUNDLED_ENGINE_VERSION } from "./generated.js"; +import { countFindings, formatMarkdown, reportMarker, shouldFail } from "./report.js"; + +const execFileAsync = promisify(execFile); + +interface GitHubContext { + owner: string; + repo: string; + eventName: string; + apiUrl: string; + event: Record; + pullNumber?: number; + sha?: string; + before?: string; + after?: string; +} + +interface Comment { + id: number; + body?: string; +} + +async function main(): Promise { + const inputs = readInputs(); + const workspace = process.env["GITHUB_WORKSPACE"] || process.cwd(); + configureBundledRulesDir(); + + const context = await readGitHubContext(); + const diff = await readDiff(context, inputs.githubToken); + const allowlist = await loadAllowlist(path.resolve(workspace, inputs.allowlistPath)); + + const result = await scan( + { diff }, + { + fileResolver: fileResolver(workspace), + allowlist, + minSeverity: inputs.minSeverity, + disabledDetectors: inputs.disabledDetectors, + logger: actionLogger(), + ...(inputs.fileTimeoutMs === undefined ? {} : { fileTimeoutMs: inputs.fileTimeoutMs }), + }, + ); + + const report = formatMarkdown(result, { + maxFindings: inputs.maxFindings, + owner: context.owner, + repo: context.repo, + engineVersion: normalizeEngineVersion(result), + ...(context.sha ? { sha: context.sha } : {}), + }); + + writeAnnotations(result.findings); + await writeStepSummary(report); + + if (inputs.comment && context.pullNumber !== undefined) { + await upsertPullRequestComment(context, inputs.githubToken, report); + } else if (inputs.comment) { + warning("comment=true is ignored outside pull_request events"); + } + + const counts = countFindings(result.findings); + const fail = shouldFail(result.findings, inputs.failOn); + await Promise.all([ + setOutput("findings-total", String(result.findings.length)), + setOutput("findings-block", String(counts.block)), + setOutput("findings-warn", String(counts.warn)), + setOutput("findings-info", String(counts.info)), + setOutput("conclusion", fail ? "fail" : "pass"), + ]); + + if (fail) { + throw new Error(`obfuscan found findings at or above fail-on=${inputs.failOn}`); + } +} + +function configureBundledRulesDir(): void { + if (process.env["OBFUSCAN_RULES_DIR"]) return; + const here = path.dirname(fileURLToPath(import.meta.url)); + process.env["OBFUSCAN_RULES_DIR"] = path.join(here, "rules", "languages"); +} + +async function readGitHubContext(): Promise { + const repository = process.env["GITHUB_REPOSITORY"]; + if (!repository || !repository.includes("/")) { + throw new Error("GITHUB_REPOSITORY is required"); + } + const [owner, repo] = repository.split("/", 2) as [string, string]; + const eventPath = process.env["GITHUB_EVENT_PATH"]; + const event = eventPath ? JSON.parse(await fs.readFile(eventPath, "utf8")) as Record : {}; + const eventName = process.env["GITHUB_EVENT_NAME"] || ""; + const pullRequest = event["pull_request"] as Record | undefined; + const baseApiUrl = process.env["GITHUB_API_URL"] || "https://api.github.com"; + const before = typeof event["before"] === "string" ? event["before"] : undefined; + const after = typeof event["after"] === "string" ? event["after"] : undefined; + + const sha = readHeadSha(event) || process.env["GITHUB_SHA"]; + + return { + owner, + repo, + eventName, + apiUrl: baseApiUrl.replace(/\/$/, ""), + event, + ...(typeof pullRequest?.["number"] === "number" ? { pullNumber: pullRequest["number"] as number } : {}), + ...(sha ? { sha } : {}), + ...(before ? { before } : {}), + ...(after ? { after } : {}), + }; +} + +function readHeadSha(event: Record): string | undefined { + const pullRequest = event["pull_request"] as Record | undefined; + const head = pullRequest?.["head"] as Record | undefined; + return typeof head?.["sha"] === "string" ? head["sha"] : undefined; +} + +async function readDiff(context: GitHubContext, token: string): Promise { + if (context.pullNumber !== undefined) { + return githubRequestText( + context, + token, + `/repos/${context.owner}/${context.repo}/pulls/${context.pullNumber}`, + "application/vnd.github.v3.diff", + ); + } + + if (context.eventName === "push" && context.before && context.after) { + return githubRequestText( + context, + token, + `/repos/${context.owner}/${context.repo}/compare/${context.before}...${context.after}`, + "application/vnd.github.diff", + ); + } + + return readLocalDiff(); +} + +async function readLocalDiff(): Promise { + try { + const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff", "--unified=0", "HEAD~1...HEAD"], { + maxBuffer: 50 * 1024 * 1024, + }); + return stdout; + } catch (err) { + throw new Error(`unable to determine a diff for this event: ${String(err)}`); + } +} + +async function loadAllowlist(filePath: string): Promise { + try { + const raw = await fs.readFile(filePath, "utf8"); + return JSON.parse(raw) as Allowlist; + } catch { + return {}; + } +} + +function fileResolver(workspace: string) { + const root = path.resolve(workspace); + return async (relativePath: string): Promise => { + const full = path.resolve(root, relativePath); + if (full !== root && !full.startsWith(root + path.sep)) return null; + try { + const stat = await fs.stat(full); + if (!stat.isFile()) return null; + return await fs.readFile(full, "utf8"); + } catch { + return null; + } + }; +} + +async function upsertPullRequestComment(context: GitHubContext, token: string, body: string): Promise { + if (!token) { + warning("github-token is empty; skipping pull request comment"); + return; + } + if (context.pullNumber === undefined) return; + + try { + const comments = await githubRequestJson( + context, + token, + `/repos/${context.owner}/${context.repo}/issues/${context.pullNumber}/comments?per_page=100`, + "GET", + ); + const existing = comments.find(comment => comment.body?.includes(reportMarker())); + if (existing) { + await githubRequestJson( + context, + token, + `/repos/${context.owner}/${context.repo}/issues/comments/${existing.id}`, + "PATCH", + { body }, + ); + } else { + await githubRequestJson( + context, + token, + `/repos/${context.owner}/${context.repo}/issues/${context.pullNumber}/comments`, + "POST", + { body }, + ); + } + } catch (err) { + warning(`failed to write pull request comment: ${String(err)}`); + } +} + +async function githubRequestText(context: GitHubContext, token: string, route: string, accept: string): Promise { + const response = await fetch(context.apiUrl + route, { + headers: githubHeaders(token, accept), + }); + if (!response.ok) { + throw new Error(`GitHub API ${route} failed with ${response.status}: ${await response.text()}`); + } + return response.text(); +} + +async function githubRequestJson( + context: GitHubContext, + token: string, + route: string, + method: "GET" | "POST" | "PATCH", + body?: unknown, +): Promise { + const response = await fetch(context.apiUrl + route, { + method, + headers: { + ...githubHeaders(token, "application/vnd.github+json"), + ...(body === undefined ? {} : { "content-type": "application/json" }), + }, + ...(body === undefined ? {} : { body: JSON.stringify(body) }), + }); + if (!response.ok) { + throw new Error(`GitHub API ${method} ${route} failed with ${response.status}: ${await response.text()}`); + } + return response.json() as Promise; +} + +function githubHeaders(token: string, accept: string): Record { + return { + accept, + "user-agent": "obfuscan-github-action", + "x-github-api-version": "2022-11-28", + ...(token ? { authorization: `Bearer ${token}` } : {}), + }; +} + +function writeAnnotations(findings: readonly Finding[]): void { + for (const finding of findings) { + const command = finding.severity === "block" ? "error" : finding.severity === "warn" ? "warning" : "notice"; + const title = finding.ruleId; + const message = `${finding.reason}\n${finding.snippet}`; + console.log(`::${command} file=${escapeProperty(finding.file)},line=${finding.line},title=${escapeProperty(title)}::${escapeData(message)}`); + } +} + +async function writeStepSummary(report: string): Promise { + const summaryPath = process.env["GITHUB_STEP_SUMMARY"]; + if (!summaryPath) return; + await fs.appendFile(summaryPath, report + "\n", "utf8"); +} + +async function setOutput(name: string, value: string): Promise { + const outputPath = process.env["GITHUB_OUTPUT"]; + if (!outputPath) { + console.log(`${name}=${value}`); + return; + } + try { + await fs.appendFile(outputPath, `${name}=${value}\n`, "utf8"); + } catch (err) { + warning(`failed to set output ${name}: ${String(err)}`); + } +} + +function actionLogger() { + return { + debug: (msg: string) => console.log(`::debug::${escapeData(msg)}`), + info: (msg: string) => console.log(msg), + warn: (msg: string) => warning(msg), + error: (msg: string) => console.log(`::error::${escapeData(msg)}`), + }; +} + +function normalizeEngineVersion(result: ScanResult): string { + return result.engineVersion === "0.0.0-source" ? BUNDLED_ENGINE_VERSION : result.engineVersion; +} + +function warning(message: string): void { + console.log(`::warning::${escapeData(message)}`); +} + +function escapeData(value: string): string { + return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); +} + +function escapeProperty(value: string): string { + return escapeData(value).replace(/:/g, "%3A").replace(/,/g, "%2C"); +} + +main().catch(err => { + console.log(`::error::${escapeData(err instanceof Error ? err.message : String(err))}`); + process.exitCode = 1; +}); diff --git a/packages/action/src/inputs.ts b/packages/action/src/inputs.ts new file mode 100644 index 0000000..4dfa4bf --- /dev/null +++ b/packages/action/src/inputs.ts @@ -0,0 +1,72 @@ +import type { Severity } from "@obfuscan/core"; + +export type FailOn = "block" | "warn" | "never"; + +export interface ActionInputs { + githubToken: string; + failOn: FailOn; + minSeverity: Severity; + comment: boolean; + maxFindings: number; + allowlistPath: string; + disabledDetectors: readonly string[]; + fileTimeoutMs?: number; +} + +export function readInputs(env: NodeJS.ProcessEnv = process.env): ActionInputs { + const fileTimeoutMs = parseOptionalPositiveInt(input(env, "file-timeout-ms"), "file-timeout-ms"); + + return { + githubToken: input(env, "github-token"), + failOn: parseFailOn(input(env, "fail-on") || "block"), + minSeverity: parseSeverity(input(env, "min-severity") || "info"), + comment: parseBoolean(input(env, "comment") || "true"), + maxFindings: parsePositiveInt(input(env, "max-findings") || "50", "max-findings"), + allowlistPath: input(env, "allowlist-path") || ".obfuscan/allowlist.json", + disabledDetectors: parseList(input(env, "disabled-detectors")), + ...(fileTimeoutMs === undefined ? {} : { fileTimeoutMs }), + }; +} + +function input(env: NodeJS.ProcessEnv, name: string): string { + const githubKey = `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; + const legacyKey = `INPUT_${name.replace(/ /g, "_").replace(/-/g, "_").toUpperCase()}`; + return (env[githubKey] ?? env[legacyKey] ?? "").trim(); +} + +function parseSeverity(value: string): Severity { + if (value === "info" || value === "warn" || value === "block") return value; + throw new Error(`min-severity must be one of info, warn, block; got ${value}`); +} + +function parseFailOn(value: string): FailOn { + if (value === "block" || value === "warn" || value === "never") return value; + throw new Error(`fail-on must be one of block, warn, never; got ${value}`); +} + +function parseBoolean(value: string): boolean { + const normalized = value.toLowerCase(); + if (["1", "true", "yes", "on"].includes(normalized)) return true; + if (["0", "false", "no", "off"].includes(normalized)) return false; + throw new Error(`boolean input must be true or false; got ${value}`); +} + +function parsePositiveInt(value: string, name: string): number { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed < 1) { + throw new Error(`${name} must be a positive integer; got ${value}`); + } + return parsed; +} + +function parseOptionalPositiveInt(value: string, name: string): number | undefined { + if (!value) return undefined; + return parsePositiveInt(value, name); +} + +function parseList(value: string): string[] { + return value + .split(/[\n,]/g) + .map(item => item.trim()) + .filter(Boolean); +} diff --git a/packages/action/src/report.ts b/packages/action/src/report.ts new file mode 100644 index 0000000..6058b34 --- /dev/null +++ b/packages/action/src/report.ts @@ -0,0 +1,129 @@ +import type { Finding, ScanResult, Severity } from "@obfuscan/core"; + +export interface ReportOptions { + maxFindings: number; + owner?: string; + repo?: string; + sha?: string; + engineVersion?: string; + rulesVersion?: string; +} + +const MARKER = ""; +const SEVERITIES: readonly Severity[] = ["block", "warn", "info"]; + +export function reportMarker(): string { + return MARKER; +} + +export function countFindings(findings: readonly Finding[]): Record { + return { + block: findings.filter(f => f.severity === "block").length, + warn: findings.filter(f => f.severity === "warn").length, + info: findings.filter(f => f.severity === "info").length, + }; +} + +export function shouldFail(findings: readonly Finding[], failOn: "block" | "warn" | "never"): boolean { + if (failOn === "never") return false; + if (failOn === "block") return findings.some(f => f.severity === "block"); + return findings.some(f => f.severity === "block" || f.severity === "warn"); +} + +export function formatMarkdown(result: ScanResult, options: ReportOptions): string { + const counts = countFindings(result.findings); + const total = result.findings.length; + const shown = result.findings.slice(0, Math.max(1, options.maxFindings)); + const omitted = Math.max(0, total - shown.length); + const status = total === 0 + ? "No findings." + : counts.block > 0 + ? "Blocking findings found." + : counts.warn > 0 + ? "Warnings found." + : "Informational findings found."; + + const lines = [ + MARKER, + "## obfuscan report", + "", + `**Status:** ${status}`, + "", + "| Metric | Value |", + "|---|---:|", + `| Scanned files | ${result.files.length} |`, + `| Findings | ${total} |`, + `| Block | ${counts.block} |`, + `| Warn | ${counts.warn} |`, + `| Info | ${counts.info} |`, + `| Duration | ${result.durationMs} ms |`, + `| Engine | ${escapeTable(options.engineVersion ?? result.engineVersion)} |`, + `| Rules | ${escapeTable(options.rulesVersion ?? result.rulesVersion)} |`, + ]; + + if (result.failedDetectors.length > 0) { + lines.push(`| Failed detectors | ${escapeTable(result.failedDetectors.join(", "))} |`); + } + + if (total === 0) { + lines.push("", "No suspicious obfuscation or backdoor patterns were found in the scanned diff."); + return lines.join("\n"); + } + + for (const severity of SEVERITIES) { + const group = shown.filter(f => f.severity === severity); + if (group.length === 0) continue; + + lines.push("", `### ${titleCase(severity)} findings`, ""); + lines.push("| Location | Rule | Score | Reason | Snippet |"); + lines.push("|---|---|---:|---|---|"); + for (const finding of group) { + lines.push(formatFindingRow(finding, options)); + } + } + + if (omitted > 0) { + lines.push("", `${omitted} additional finding${omitted === 1 ? "" : "s"} omitted from this comment.`); + } + + lines.push("", "Suppress a known false positive with an in-source `obfuscan-disable-next-line` directive or `.obfuscan/allowlist.json`."); + + return lines.join("\n"); +} + +function formatFindingRow(finding: Finding, options: ReportOptions): string { + const location = formatLocation(finding, options); + const snippet = inlineCode(finding.snippet.replace(/\s+/g, " ").slice(0, 160)); + return [ + location, + inlineCode(finding.ruleId), + String(finding.score), + escapeTable(finding.reason), + snippet, + ].join(" | ").replace(/^/, "|").replace(/$/, "|"); +} + +function formatLocation(finding: Finding, options: ReportOptions): string { + const label = `${finding.file}:${finding.line}`; + if (!options.owner || !options.repo || !options.sha) return escapeTable(label); + const path = finding.file.split("/").map(encodeURIComponent).join("/"); + return `[${escapeTable(label)}](https://github.com/${options.owner}/${options.repo}/blob/${options.sha}/${path}#L${finding.line})`; +} + +function inlineCode(value: string): string { + const escaped = escapeTable(value).replace(/`/g, "`"); + return `${escaped}`; +} + +function escapeTable(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\|/g, "|") + .replace(/\r?\n/g, "
"); +} + +function titleCase(value: string): string { + return value.slice(0, 1).toUpperCase() + value.slice(1); +} diff --git a/packages/action/test/inputs.test.ts b/packages/action/test/inputs.test.ts new file mode 100644 index 0000000..e36aa66 --- /dev/null +++ b/packages/action/test/inputs.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { readInputs } from "../src/inputs"; + +describe("readInputs", () => { + it("reads GitHub action input environment variables with hyphenated names", () => { + const inputs = readInputs({ + "INPUT_GITHUB-TOKEN": "token-1", + "INPUT_FAIL-ON": "warn", + "INPUT_MIN-SEVERITY": "block", + "INPUT_COMMENT": "false", + "INPUT_MAX-FINDINGS": "7", + "INPUT_ALLOWLIST-PATH": "custom/allowlist.json", + "INPUT_DISABLED-DETECTORS": "obf.long-line\nobf.high-entropy-literal", + "INPUT_FILE-TIMEOUT-MS": "1234", + }); + + expect(inputs).toEqual({ + githubToken: "token-1", + failOn: "warn", + minSeverity: "block", + comment: false, + maxFindings: 7, + allowlistPath: "custom/allowlist.json", + disabledDetectors: ["obf.long-line", "obf.high-entropy-literal"], + fileTimeoutMs: 1234, + }); + }); + + it("keeps underscore input names as a compatibility fallback", () => { + const inputs = readInputs({ + "INPUT_GITHUB_TOKEN": "token-2", + "INPUT_FAIL_ON": "never", + "INPUT_MIN_SEVERITY": "warn", + "INPUT_COMMENT": "true", + "INPUT_MAX_FINDINGS": "12", + "INPUT_ALLOWLIST_PATH": "fallback.json", + "INPUT_DISABLED_DETECTORS": "obf.a,obf.b", + "INPUT_FILE_TIMEOUT_MS": "4321", + }); + + expect(inputs).toEqual({ + githubToken: "token-2", + failOn: "never", + minSeverity: "warn", + comment: true, + maxFindings: 12, + allowlistPath: "fallback.json", + disabledDetectors: ["obf.a", "obf.b"], + fileTimeoutMs: 4321, + }); + }); + + it("prefers GitHub hyphenated input names over underscore fallbacks", () => { + const inputs = readInputs({ + "INPUT_GITHUB-TOKEN": "official-token", + "INPUT_GITHUB_TOKEN": "fallback-token", + }); + + expect(inputs.githubToken).toBe("official-token"); + }); +}); diff --git a/packages/action/test/report.test.ts b/packages/action/test/report.test.ts new file mode 100644 index 0000000..8b073b3 --- /dev/null +++ b/packages/action/test/report.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vitest"; +import type { ScanResult } from "@obfuscan/core"; +import { countFindings, formatMarkdown, reportMarker, shouldFail } from "../src/report"; + +const result: ScanResult = { + findings: [ + { + ruleId: "obf.decode-then-exec.javascript", + severity: "block", + score: 10, + file: "src/a.js", + line: 12, + snippet: "eval(atob(payload))", + reason: "Decoded data is being executed via a dynamic sink.", + }, + { + ruleId: "obf.high-entropy-literal", + severity: "warn", + score: 6, + file: "src/b.js", + line: 3, + snippet: "const token = 'abc|def';", + reason: "High entropy string literal.", + }, + ], + files: [{ path: "src/a.js", languageId: "javascript" }], + durationMs: 42, + failedDetectors: [], + rulesVersion: "2026.04.0", + engineVersion: "0.2.0", +}; + +describe("report formatting", () => { + it("renders a marked Markdown report", () => { + const markdown = formatMarkdown(result, { + maxFindings: 10, + owner: "ByteBardOrg", + repo: "obfuscan", + sha: "abc123", + }); + + expect(markdown).toContain(reportMarker()); + expect(markdown).toContain("## obfuscan report"); + expect(markdown).toContain("Blocking findings found."); + expect(markdown).toContain("src/a.js:12"); + expect(markdown).toContain("obf.decode-then-exec.javascript"); + expect(markdown).toContain("abc|def"); + }); + + it("counts findings by severity", () => { + expect(countFindings(result.findings)).toEqual({ block: 1, warn: 1, info: 0 }); + }); + + it("applies fail-on thresholds", () => { + expect(shouldFail(result.findings, "block")).toBe(true); + expect(shouldFail(result.findings, "warn")).toBe(true); + expect(shouldFail(result.findings, "never")).toBe(false); + }); +}); diff --git a/packages/action/test/vitest.config.ts b/packages/action/test/vitest.config.ts new file mode 100644 index 0000000..479b7f2 --- /dev/null +++ b/packages/action/test/vitest.config.ts @@ -0,0 +1,9 @@ +/// +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + exclude: ["node_modules/**", "dist/**"], + }, +}); diff --git a/packages/action/tsconfig.json b/packages/action/tsconfig.json new file mode 100644 index 0000000..409c0aa --- /dev/null +++ b/packages/action/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "isolatedModules": true, + "resolveJsonModule": true, + "declaration": false, + "noEmit": true + }, + "include": ["src", "test"] +} diff --git a/packages/core/README.md b/packages/core/README.md index 9149d3e..bd4750a 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -53,6 +53,45 @@ pnpm add @obfuscan/core @obfuscan/rules The `core` package ships the engine; `rules` ships language configs and tree-sitter query assets, not parser grammars. Hosts that want parser-backed custom detectors provide their own grammars via `RuleSet.loadGrammar()` / `GrammarHandle.parse()`. We use SemVer for the engine and CalVer (`2026.04.0`) for the rules. +## GitHub Action + +Run obfuscan directly in pull request CI and have it annotate findings, write a job summary, and upsert a Markdown PR comment. + +```yaml +name: obfuscan + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + issues: write + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: ByteBardOrg/obfuscan-action@v1 + with: + fail-on: block +``` + +Useful inputs: + +- `fail-on`: `block` (default), `warn`, or `never`. +- `min-severity`: `info` (default), `warn`, or `block`. +- `comment`: `true` (default) or `false`. +- `max-findings`: maximum findings shown in the Markdown report, default `50`. +- `disabled-detectors`: comma or newline separated detector ids. +- `allowlist-path`: defaults to `.obfuscan/allowlist.json`. + +The Marketplace action is distributed from `ByteBardOrg/obfuscan-action`. Generate that repo's contents locally with `cd packages/action && npm run marketplace`; the generated `marketplace/obfuscan-action` folder is ignored in this repo. + ## Using `@obfuscan/rules` `@obfuscan/core` loads language configs from `@obfuscan/rules` by default, so normal usage is just installing both packages. @@ -205,7 +244,7 @@ Pre-1.0. The detector framework, scoring, suppression, and tier-1/tier-2 languag - [x] Manifest detectors for npm, PyPI, GitHub Actions, Dockerfile - [x] Tier-2 language rules (Go, Rust, C#, Java, Kotlin, Lua, Perl, VBScript) - [ ] `@obfuscan/cli` 1.0 with SARIF output -- [ ] `@obfuscan/github-action` +- [x] `@obfuscan/github-action` v1 - [ ] `@obfuscan/llm-verify` optional Layer-D package - [ ] Reproducible benchmark suite against [Datadog malicious-software-packages-dataset](https://github.com/DataDog/malicious-software-packages-dataset) diff --git a/packages/rules/README.md b/packages/rules/README.md index 9149d3e..bd4750a 100644 --- a/packages/rules/README.md +++ b/packages/rules/README.md @@ -53,6 +53,45 @@ pnpm add @obfuscan/core @obfuscan/rules The `core` package ships the engine; `rules` ships language configs and tree-sitter query assets, not parser grammars. Hosts that want parser-backed custom detectors provide their own grammars via `RuleSet.loadGrammar()` / `GrammarHandle.parse()`. We use SemVer for the engine and CalVer (`2026.04.0`) for the rules. +## GitHub Action + +Run obfuscan directly in pull request CI and have it annotate findings, write a job summary, and upsert a Markdown PR comment. + +```yaml +name: obfuscan + +on: + pull_request: + +permissions: + contents: read + pull-requests: read + issues: write + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + + - uses: ByteBardOrg/obfuscan-action@v1 + with: + fail-on: block +``` + +Useful inputs: + +- `fail-on`: `block` (default), `warn`, or `never`. +- `min-severity`: `info` (default), `warn`, or `block`. +- `comment`: `true` (default) or `false`. +- `max-findings`: maximum findings shown in the Markdown report, default `50`. +- `disabled-detectors`: comma or newline separated detector ids. +- `allowlist-path`: defaults to `.obfuscan/allowlist.json`. + +The Marketplace action is distributed from `ByteBardOrg/obfuscan-action`. Generate that repo's contents locally with `cd packages/action && npm run marketplace`; the generated `marketplace/obfuscan-action` folder is ignored in this repo. + ## Using `@obfuscan/rules` `@obfuscan/core` loads language configs from `@obfuscan/rules` by default, so normal usage is just installing both packages. @@ -205,7 +244,7 @@ Pre-1.0. The detector framework, scoring, suppression, and tier-1/tier-2 languag - [x] Manifest detectors for npm, PyPI, GitHub Actions, Dockerfile - [x] Tier-2 language rules (Go, Rust, C#, Java, Kotlin, Lua, Perl, VBScript) - [ ] `@obfuscan/cli` 1.0 with SARIF output -- [ ] `@obfuscan/github-action` +- [x] `@obfuscan/github-action` v1 - [ ] `@obfuscan/llm-verify` optional Layer-D package - [ ] Reproducible benchmark suite against [Datadog malicious-software-packages-dataset](https://github.com/DataDog/malicious-software-packages-dataset)