commit fb175822b7790a8d41c2bd4f6fc86cf7b6115f67 Author: Bun Bun Date: Thu Jun 18 18:27:39 2026 +0000 feat: Vibe-Coded SaaS Security Scanner v1.0.0 - automated security scanner for vibe-coded AI-generated SaaS apps diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ef9d37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules/ +dist/ +*.log +.verdict +.venv/ +.vite/ +.DS_Store +*.swp +*.swo +*~ +.idea/ +.vscode/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e1dbbc --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# Vibe-Coded SaaS Security Scanner + +Automated security scanner for vibe-coded / AI-generated SaaS apps. Detects exposed API keys, missing input validation, and insecure client-side secrets. + +## Install + +```bash +npm install -g vibe-coded-saas-security-scanner +``` + +## Usage + +```bash +# Scan current directory +vibe-scan . + +# Scan specific paths with JSON output +vibe-scan --format json --output report.json ./src ./lib + +# Only show critical and high severity findings +vibe-scan --min-severity high ./src + +# Only scan for secrets +vibe-scan --categories secret ./src +``` + +## Output Formats + +- `text` (default) — human-readable colored terminal output +- `json` — structured JSON for CI integration +- `sarif` — SARIF v2.1.0 for GitHub/CodeQL integration +- `markdown` — Markdown report for PR comments + +## Exit Codes + +- `0` — No critical findings +- `1` — Scanner error +- `2` — At least one critical finding detected + +## Categories + +- **secret** — Exposed API keys, tokens, passwords, private keys +- **injection** — SQL injection, NoSQL injection, command injection, path traversal +- **client-side** — DOM XSS, dangerous eval, dangerouslySetInnerHTML +- **validation** — Missing input validation, mass assignment, insecure uploads +- **config** — Missing CORS, insecure cookies, debug mode, HTTP instead of HTTPS + +## Rules + +The scanner includes 25+ detection rules covering: + +- Stripe, AWS, OpenAI, Twilio, SendGrid, GitHub, Slack tokens +- Database connection strings +- JWT secrets and session keys +- Private keys +- SQL / NoSQL / Command injection patterns +- Path traversal +- XSS and dangerous DOM operations +- Missing CORS, cookie, rate limiting configurations +- Insecure file uploads +- Debug mode in production + +## CI Integration + +```yaml +- name: Security Scan + run: | + npx vibe-coded-saas-security-scanner --format sarif --output security.sarif . +- name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: security.sarif +``` + +## License + +MIT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..af3d9d2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1887 @@ +{ + "name": "vibe-coded-saas-security-scanner", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vibe-coded-saas-security-scanner", + "version": "1.0.0", + "license": "MIT", + "bin": { + "vibe-scan": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^20.14.0", + "typescript": "^5.5.0", + "vitest": "^1.6.0" + } + }, + "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/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "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.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "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.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "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.13", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", + "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "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.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "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/package.json b/package.json new file mode 100644 index 0000000..c31814e --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "vibe-coded-saas-security-scanner", + "version": "1.0.0", + "description": "Automated security scanner for vibe-coded / AI-generated SaaS apps. Detects exposed API keys, missing input validation, and insecure client-side secrets.", + "type": "module", + "main": "dist/index.js", + "bin": { + "vibe-scan": "dist/cli.js" + }, + "scripts": { + "build": "tsc", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "keywords": ["security", "scanner", "saas", "ai-generated", "vibe-coding"], + "author": "BunBun Labs", + "license": "MIT", + "devDependencies": { + "typescript": "^5.5.0", + "vitest": "^1.6.0", + "@types/node": "^20.14.0" + }, + "dependencies": {} +} diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..68113b2 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,186 @@ +#!/usr/bin/env node +import { scan } from './scanner.js'; +import { report } from './reporter.js'; +import { writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +interface CliArgs { + paths: string[]; + format: 'text' | 'json' | 'sarif' | 'markdown'; + output?: string; + exclude: string[]; + rules?: string[]; + categories?: string[]; + severities?: string[]; + maxFileSize: number; + noGitignore: boolean; + showSnippet: boolean; + minSeverity?: string; + help: boolean; + version: boolean; +} + +function parseArgs(): CliArgs { + const args = process.argv.slice(2); + const parsed: CliArgs = { + paths: [], + format: 'text', + exclude: [], + maxFileSize: 1024 * 1024, + noGitignore: false, + showSnippet: true, + help: false, + version: false, + }; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + switch (arg) { + case '-h': + case '--help': + parsed.help = true; + break; + case '-v': + case '--version': + parsed.version = true; + break; + case '-f': + case '--format': + parsed.format = (args[++i] as CliArgs['format']) || 'text'; + break; + case '-o': + case '--output': + parsed.output = args[++i]; + break; + case '-e': + case '--exclude': + parsed.exclude.push(args[++i]); + break; + case '-r': + case '--rules': + parsed.rules = (args[++i] || '').split(',').filter(Boolean); + break; + case '-c': + case '--categories': + parsed.categories = (args[++i] || '').split(',').filter(Boolean); + break; + case '-s': + case '--severities': + parsed.severities = (args[++i] || '').split(',').filter(Boolean); + break; + case '--max-file-size': + parsed.maxFileSize = parseInt(args[++i], 10) || 1024 * 1024; + break; + case '--no-gitignore': + parsed.noGitignore = true; + break; + case '--no-snippet': + parsed.showSnippet = false; + break; + case '--min-severity': + parsed.minSeverity = args[++i]; + break; + default: + if (arg.startsWith('-')) { + console.error(`Unknown option: ${arg}`); + process.exit(1); + } else { + parsed.paths.push(arg); + } + } + } + + return parsed; +} + +function printHelp(): void { + console.log(` +Vibe-Coded SaaS Security Scanner +Automated security scanner for vibe-coded / AI-generated SaaS apps. + +Usage: vibe-scan [options] + +Options: + -h, --help Show this help + -v, --version Show version + -f, --format Output format: text (default), json, sarif, markdown + -o, --output Write output to file instead of stdout + -e, --exclude Exclude paths matching pattern (repeatable) + -r, --rules Comma-separated rule IDs to run + -c, --categories Comma-separated categories: secret,injection,validation,client-side,config + -s, --severities Comma-separated severities: critical,high,medium,low + --max-file-size Skip files larger than this (default: 1MB) + --no-gitignore Do not respect .gitignore files + --no-snippet Hide code snippets in output + --min-severity Only show findings at or above this severity + +Examples: + vibe-scan . + vibe-scan --format json --output report.json ./src + vibe-scan --categories secret --min-severity high ./src ./lib +`); +} + +function printVersion(): void { + console.log('vibe-coded-saas-security-scanner v1.0.0'); +} + +async function main(): Promise { + const args = parseArgs(); + + if (args.help) { + printHelp(); + process.exit(0); + } + + if (args.version) { + printVersion(); + process.exit(0); + } + + if (args.paths.length === 0) { + console.error('Error: No paths specified. Use --help for usage.'); + process.exit(1); + } + + const resolvedPaths = args.paths.map((p) => resolve(p)); + + console.log('🔍 Vibe-Coded SaaS Security Scanner'); + console.log(' Scanning:', resolvedPaths.join(', ')); + console.log(''); + + const result = await scan({ + paths: resolvedPaths, + exclude: args.exclude, + rules: args.rules, + categories: args.categories, + severities: args.severities, + maxFileSize: args.maxFileSize, + respectGitignore: !args.noGitignore, + }); + + const output = report(result, { + format: args.format, + output: args.output, + showSnippet: args.showSnippet, + minSeverity: args.minSeverity, + }); + + if (args.output) { + await writeFile(args.output, output, 'utf-8'); + console.log(`\nReport written to ${args.output}`); + } else { + console.log(output); + } + + // Exit with non-zero if critical findings exist + const hasCritical = result.findings.some((f) => f.severity === 'critical'); + if (hasCritical) { + process.exit(2); + } +} + +main().catch((err) => { + console.error('Error:', err); + process.exit(1); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d096545 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,3 @@ +export { scan, type ScanOptions, type ScanResult, type Finding } from './scanner.js'; +export { report, type Format, type ReporterOptions } from './reporter.js'; +export { rules, type Rule, getRulesByCategory, getRulesBySeverity } from './rules.js'; diff --git a/src/reporter.test.ts b/src/reporter.test.ts new file mode 100644 index 0000000..7e6cc2d --- /dev/null +++ b/src/reporter.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest'; +import { report } from '../src/reporter.js'; +import type { ScanResult, Finding } from '../src/scanner.js'; + +function makeResult(findings: Finding[]): ScanResult { + return { + findings, + filesScanned: 3, + filesSkipped: 0, + rulesRun: 10, + durationMs: 42, + }; +} + +const sampleFinding: Finding = { + ruleId: 'SEC-001', + ruleName: 'Exposed Stripe Secret Key', + severity: 'critical', + category: 'secret', + description: 'Stripe secret key detected in source.', + file: '/src/payments.js', + line: 5, + column: 20, + snippet: "...require('stripe')('sk_live_...')...", + match: "sk_live_abcdefghijklmnopqrstuvwxyz", +}; + +describe('reporter', () => { + it('should format text output', () => { + const result = makeResult([sampleFinding]); + const output = report(result, { format: 'text' }); + expect(output).toContain('Exposed Stripe Secret Key'); + expect(output).toContain('CRITICAL'); + expect(output).toContain('Files scanned: 3'); + }); + + it('should format JSON output', () => { + const result = makeResult([sampleFinding]); + const output = report(result, { format: 'json' }); + const parsed = JSON.parse(output); + expect(parsed.version).toBe('1.0.0'); + expect(parsed.findings.length).toBe(1); + expect(parsed.findings[0].ruleId).toBe('SEC-001'); + expect(parsed.summary.filesScanned).toBe(3); + }); + + it('should format markdown output', () => { + const result = makeResult([sampleFinding]); + const output = report(result, { format: 'markdown' }); + expect(output).toContain('# Vibe-Coded SaaS Security Scanner Report'); + expect(output).toContain('### /src/payments.js'); + expect(output).toContain('Exposed Stripe Secret Key'); + }); + + it('should format SARIF output', () => { + const result = makeResult([sampleFinding]); + const output = report(result, { format: 'sarif' }); + const parsed = JSON.parse(output); + expect(parsed.version).toBe('2.1.0'); + expect(parsed.runs[0].results.length).toBe(1); + expect(parsed.runs[0].results[0].level).toBe('error'); + }); + + it('should filter by min severity', () => { + const findings: Finding[] = [ + { ...sampleFinding, severity: 'critical' }, + { ...sampleFinding, ruleId: 'SEC-002', severity: 'low', ruleName: 'Low Priority', description: 'low' }, + ]; + const result = makeResult(findings); + const output = report(result, { format: 'text', minSeverity: 'high' }); + expect(output).toContain('CRITICAL'); + }); + + it('should show empty results', () => { + const result = makeResult([]); + const output = report(result, { format: 'text' }); + expect(output).toContain('No security issues found'); + }); +}); diff --git a/src/reporter.ts b/src/reporter.ts new file mode 100644 index 0000000..301f971 --- /dev/null +++ b/src/reporter.ts @@ -0,0 +1,237 @@ +import type { Finding, ScanResult } from './scanner.js'; + +export type Format = 'text' | 'json' | 'sarif' | 'markdown'; + +export interface ReporterOptions { + format: Format; + output?: string; + showSnippet?: boolean; + minSeverity?: string; +} + +const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 }; +const SEVERITY_ICONS = { critical: '🔴', high: '🟠', medium: '🟡', low: '🟢' }; +const SEVERITY_COLORS = { critical: '\x1b[31m', high: '\x1b[33m', medium: '\x1b[36m', low: '\x1b[32m' }; +const RESET = '\x1b[0m'; + +function severityRank(s: string): number { + return SEVERITY_ORDER[s as keyof typeof SEVERITY_ORDER] ?? 99; +} + +function filterFindings(findings: Finding[], minSeverity?: string): Finding[] { + if (!minSeverity) return findings; + const minRank = severityRank(minSeverity); + return findings.filter((f) => severityRank(f.severity) <= minRank); +} + +function sortFindings(findings: Finding[]): Finding[] { + return [...findings].sort((a, b) => { + const sevDiff = severityRank(a.severity) - severityRank(b.severity); + if (sevDiff !== 0) return sevDiff; + if (a.file !== b.file) return a.file.localeCompare(b.file); + return a.line - b.line; + }); +} + +function formatText(result: ScanResult, opts: ReporterOptions): string { + let out = ''; + const findings = sortFindings(filterFindings(result.findings, opts.minSeverity)); + + out += '\n'; + out += '╔══════════════════════════════════════════════════════════════╗\n'; + out += '║ Vibe-Coded SaaS Security Scanner - Results ║\n'; + out += '╚══════════════════════════════════════════════════════════════╝\n'; + out += '\n'; + out += `Files scanned: ${result.filesScanned}\n`; + out += `Files skipped: ${result.filesSkipped}\n`; + out += `Rules run: ${result.rulesRun}\n`; + out += `Duration: ${result.durationMs}ms\n`; + out += `\n`; + + if (findings.length === 0) { + out += '✅ No security issues found.\n'; + return out; + } + + // Summary by severity + const counts = { critical: 0, high: 0, medium: 0, low: 0 }; + for (const f of findings) { + counts[f.severity] = (counts[f.severity] || 0) + 1; + } + out += 'Summary:\n'; + for (const [sev, count] of Object.entries(counts)) { + if (count > 0) { + const icon = SEVERITY_ICONS[sev as keyof typeof SEVERITY_ICONS]; + const color = SEVERITY_COLORS[sev as keyof typeof SEVERITY_COLORS]; + out += ` ${icon} ${color}${sev.toUpperCase()}${RESET}: ${count}\n`; + } + } + out += `\n`; + out += `Found ${findings.length} issue(s):\n`; + out += `\n`; + + let currentFile = ''; + for (const f of findings) { + if (f.file !== currentFile) { + currentFile = f.file; + out += `\n📁 ${f.file}\n`; + out += '─'.repeat(60) + '\n'; + } + const color = SEVERITY_COLORS[f.severity] || ''; + const icon = SEVERITY_ICONS[f.severity] || '⚪'; + out += ` ${icon} ${color}[${f.severity.toUpperCase()}]${RESET} ${f.ruleName} (${f.ruleId})\n`; + out += ` Line ${f.line}, Col ${f.column}\n`; + if (opts.showSnippet !== false) { + out += ` ${f.snippet}\n`; + } + out += ` ${f.description}\n`; + } + + out += '\n'; + out += '────────────────────────────────────────────────────────────\n'; + out += '💡 Tip: Fix CRITICAL issues immediately. HIGH issues within 24h.\n'; + out += ' Run with --format json for CI integration.\n'; + + return out; +} + +function formatJson(result: ScanResult, opts: ReporterOptions): string { + const findings = sortFindings(filterFindings(result.findings, opts.minSeverity)); + const payload = { + version: '1.0.0', + scanner: 'vibe-coded-saas-security-scanner', + summary: { + filesScanned: result.filesScanned, + filesSkipped: result.filesSkipped, + rulesRun: result.rulesRun, + durationMs: result.durationMs, + totalFindings: findings.length, + severityCounts: findings.reduce((acc, f) => { + acc[f.severity] = (acc[f.severity] || 0) + 1; + return acc; + }, {} as Record), + }, + findings: findings.map((f) => ({ + ruleId: f.ruleId, + ruleName: f.ruleName, + severity: f.severity, + category: f.category, + description: f.description, + location: { + file: f.file, + line: f.line, + column: f.column, + }, + snippet: f.snippet, + match: f.match, + })), + }; + return JSON.stringify(payload, null, 2); +} + +function formatMarkdown(result: ScanResult, opts: ReporterOptions): string { + const findings = sortFindings(filterFindings(result.findings, opts.minSeverity)); + let out = '# Vibe-Coded SaaS Security Scanner Report\n\n'; + out += `| Metric | Value |\n`; + out += `|--------|-------|\n`; + out += `| Files Scanned | ${result.filesScanned} |\n`; + out += `| Files Skipped | ${result.filesSkipped} |\n`; + out += `| Rules Run | ${result.rulesRun} |\n`; + out += `| Duration | ${result.durationMs}ms |\n`; + out += `| Total Findings | ${findings.length} |\n\n`; + + if (findings.length === 0) { + out += '✅ No security issues found.\n'; + return out; + } + + const counts = { critical: 0, high: 0, medium: 0, low: 0 }; + for (const f of findings) { + counts[f.severity] = (counts[f.severity] || 0) + 1; + } + out += '## Severity Summary\n\n'; + for (const [sev, count] of Object.entries(counts)) { + if (count > 0) out += `- **${sev.toUpperCase()}**: ${count}\n`; + } + out += '\n'; + + out += '## Findings\n\n'; + let currentFile = ''; + for (const f of findings) { + if (f.file !== currentFile) { + currentFile = f.file; + out += `### ${f.file}\n\n`; + } + out += `#### ${f.ruleName} (${f.ruleId})\n\n`; + out += `- **Severity**: ${f.severity.toUpperCase()}\n`; + out += `- **Category**: ${f.category}\n`; + out += `- **Location**: Line ${f.line}, Column ${f.column}\n`; + out += `- **Description**: ${f.description}\n`; + if (opts.showSnippet !== false) { + out += `- **Snippet**: \`${f.snippet}\`\n`; + } + out += '\n'; + } + + return out; +} + +function formatSarif(result: ScanResult, opts: ReporterOptions): string { + const findings = sortFindings(filterFindings(result.findings, opts.minSeverity)); + const sarif = { + $schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json', + version: '2.1.0', + runs: [ + { + tool: { + driver: { + name: 'vibe-coded-saas-security-scanner', + version: '1.0.0', + informationUri: 'https://bunbunlabs.com', + rules: findings.map((f) => ({ + id: f.ruleId, + name: f.ruleName, + shortDescription: { text: f.description }, + defaultConfiguration: { level: f.severity }, + })), + }, + }, + results: findings.map((f) => ({ + ruleId: f.ruleId, + level: f.severity === 'critical' || f.severity === 'high' ? 'error' : f.severity === 'medium' ? 'warning' : 'note', + message: { text: f.description }, + locations: [ + { + physicalLocation: { + artifactLocation: { uri: f.file }, + region: { + startLine: f.line, + startColumn: f.column, + }, + }, + }, + ], + properties: { + category: f.category, + snippet: f.snippet, + }, + })), + }, + ], + }; + return JSON.stringify(sarif, null, 2); +} + +export function report(result: ScanResult, opts: ReporterOptions): string { + switch (opts.format) { + case 'json': + return formatJson(result, opts); + case 'sarif': + return formatSarif(result, opts); + case 'markdown': + return formatMarkdown(result, opts); + case 'text': + default: + return formatText(result, opts); + } +} diff --git a/src/rules.ts b/src/rules.ts new file mode 100644 index 0000000..7856e91 --- /dev/null +++ b/src/rules.ts @@ -0,0 +1,455 @@ +export interface Rule { + id: string; + name: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + category: 'secret' | 'injection' | 'validation' | 'client-side' | 'config'; + description: string; + patterns: RegExp[]; + fileFilter?: (filename: string) => boolean; + exclude?: RegExp[]; +} + +function codeFileFilter(filename: string): boolean { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + const codeExts = ['js', 'ts', 'jsx', 'tsx', 'py', 'rb', 'go', 'java', 'php', 'cs', 'rs', 'c', 'cpp', 'h', 'swift', 'kt', 'scala', 'html', 'vue', 'svelte']; + return codeExts.includes(ext); +} + +export const rules: Rule[] = [ + // === SECRETS / API KEYS === + { + id: 'SEC-001', + name: 'Exposed Stripe Secret Key', + severity: 'critical', + category: 'secret', + description: 'Stripe secret key (sk_live_ or sk_test_) detected in source code. These grant full API access to your Stripe account.', + patterns: [ + /sk_live_[a-zA-Z0-9]{24,}/g, + /sk_test_[a-zA-Z0-9]{24,}/g, + /["']sk_live_[a-zA-Z0-9]{24,}["']/g, + /["']sk_test_[a-zA-Z0-9]{24,}["']/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./], + }, + { + id: 'SEC-002', + name: 'Exposed Stripe Publishable Key in Backend', + severity: 'medium', + category: 'secret', + description: 'Stripe publishable key (pk_live_) found in non-frontend code. While not secret, should be in environment config.', + patterns: [ + /pk_live_[a-zA-Z0-9]{24,}/g, + /pk_test_[a-zA-Z0-9]{24,}/g, + ], + fileFilter: (f) => !f.includes('frontend') && !f.includes('client') && !f.endsWith('.html') && !f.endsWith('.jsx') && !f.endsWith('.tsx') && !f.endsWith('.vue'), + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./], + }, + { + id: 'SEC-003', + name: 'Exposed AWS Access Key ID', + severity: 'critical', + category: 'secret', + description: 'AWS Access Key ID (AKIA...) detected in source code. Paired with a secret key, this grants AWS account access.', + patterns: [ + /AKIA[0-9A-Z]{16}/g, + /["']AKIA[0-9A-Z]{16}["']/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-004', + name: 'Exposed AWS Secret Access Key', + severity: 'critical', + category: 'secret', + description: 'AWS Secret Access Key pattern detected in source code. Combined with Access Key ID, grants full AWS account access.', + patterns: [ + /aws_secret_access_key\s*[:=]\s*["'][a-zA-Z0-9/+=]{40}["']/gi, + /secret_access_key\s*[:=]\s*["'][a-zA-Z0-9/+=]{40}["']/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-005', + name: 'Exposed OpenAI API Key', + severity: 'critical', + category: 'secret', + description: 'OpenAI API key (sk-...) detected in source code. Grants access to GPT models and billing.', + patterns: [ + /sk-[a-zA-Z0-9]{48}/g, + /sk-proj-[a-zA-Z0-9-_]{100,}/g, + /sk-[a-zA-Z0-9]{20,}/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-006', + name: 'Exposed Generic API Key / Token', + severity: 'high', + category: 'secret', + description: 'Generic API key or token pattern detected. May be a service credential hardcoded in source.', + patterns: [ + /api[_-]?key\s*[:=]\s*["'][a-zA-Z0-9_-]{32,}["']/gi, + /apikey\s*[:=]\s*["'][a-zA-Z0-9_-]{32,}["']/gi, + /api[_-]?token\s*[:=]\s*["'][a-zA-Z0-9_-]{32,}["']/gi, + /auth[_-]?token\s*[:=]\s*["'][a-zA-Z0-9_-]{32,}["']/gi, + /bearer\s+[a-zA-Z0-9_-]{32,}/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/, /mock/, /test/], + }, + { + id: 'SEC-007', + name: 'Exposed Database Connection String', + severity: 'critical', + category: 'secret', + description: 'Database connection string with credentials detected in source code.', + patterns: [ + /(postgres|mysql|mongodb|redis)[:/][/][^/\s:]+:[^/\s@]+@[a-zA-Z0-9.-]+/gi, + /DATABASE_URL\s*[:=]\s*["'][^"']+\/\/[^"']+:[^"']+@[^"']+/gi, + /mongodb\+srv:\/\/[^/\s:]+:[^/\s@]+@/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /localhost/, /127\.0\.0\.1/, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-008', + name: 'Exposed Twilio Auth Token', + severity: 'critical', + category: 'secret', + description: 'Twilio Auth Token detected in source code. Grants SMS/voice account access.', + patterns: [ + /twilio_auth_token\s*[:=]\s*["'][a-f0-9]{32}["']/gi, + /auth_token\s*[:=]\s*["'][a-f0-9]{32}["']/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-009', + name: 'Exposed SendGrid API Key', + severity: 'high', + category: 'secret', + description: 'SendGrid API key (SG.xxx) detected in source code. Grants email sending access.', + patterns: [ + /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-010', + name: 'Exposed GitHub Personal Access Token', + severity: 'critical', + category: 'secret', + description: 'GitHub Personal Access Token (ghp_ or github_pat_) detected in source code.', + patterns: [ + /ghp_[a-zA-Z0-9]{36}/g, + /github_pat_[a-zA-Z0-9_-]{22}_[a-zA-Z0-9]{59}/g, + /gho_[a-zA-Z0-9]{36}/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-011', + name: 'Exposed JWT Secret', + severity: 'critical', + category: 'secret', + description: 'JWT signing secret detected in source code. Attackers can forge tokens with this.', + patterns: [ + /jwt[_-]?secret\s*[:=]\s*["'][^"']{16,}["']/gi, + /jwt[_-]?key\s*[:=]\s*["'][^"']{16,}["']/gi, + /secret[_-]?key\s*[:=]\s*["'][^"']{16,}["']/gi, + /SESSION_SECRET\s*[:=]\s*["'][^"']{16,}["']/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/, /generateSecret/, /crypto\.random/], + }, + { + id: 'SEC-012', + name: 'Exposed Private Key', + severity: 'critical', + category: 'secret', + description: 'Private key material detected in source code. This is the most critical secret exposure.', + patterns: [ + /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, + /-----BEGIN RSA PRIVATE KEY-----/g, + /-----BEGIN EC PRIVATE KEY-----/g, + /-----BEGIN OPENSSH PRIVATE KEY-----/g, + ], + }, + { + id: 'SEC-013', + name: 'Exposed Password in Source', + severity: 'critical', + category: 'secret', + description: 'Hardcoded password detected in source code.', + patterns: [ + /password\s*[:=]\s*["'][^"']{8,}["']/gi, + /passwd\s*[:=]\s*["'][^"']{8,}["']/gi, + /pwd\s*[:=]\s*["'][^"']{8,}["']/gi, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/, /password123/, /admin123/, /12345678/, /password\s*[:=]\s*["']\*+["']/], + }, + { + id: 'SEC-014', + name: 'Exposed Slack Token', + severity: 'high', + category: 'secret', + description: 'Slack token (xoxb- or xoxp-) detected in source code.', + patterns: [ + /xoxb-[a-zA-Z0-9-]{18,}/g, + /xoxp-[a-zA-Z0-9-]{18,}/g, + /xoxa-[a-zA-Z0-9-]{18,}/g, + ], + exclude: [/process\.env\./, /import\.meta\.env\./, /ENV\[/, /\$env\./, /example/, /placeholder/, /dummy/], + }, + { + id: 'SEC-015', + name: 'Exposed Firebase Config with API Key', + severity: 'high', + category: 'secret', + description: 'Firebase configuration object with API key detected in client-side code. While Firebase keys are less sensitive, they should not be exposed unnecessarily.', + patterns: [ + /apiKey\s*:\s*["'][A-Za-z0-9_-]{39}["']/g, + ], + fileFilter: (f) => f.endsWith('.js') || f.endsWith('.ts') || f.endsWith('.jsx') || f.endsWith('.tsx') || f.endsWith('.html') || f.endsWith('.vue') || f.endsWith('.svelte'), + }, + // === INJECTION VULNERABILITIES === + { + id: 'INJ-001', + name: 'SQL Injection Vulnerability', + severity: 'critical', + category: 'injection', + description: 'Potential SQL injection: user input concatenated into SQL query without parameterization.', + patterns: [ + /query\s*\(\s*[`"'].*\$\{.*\}.*[`"']/g, + /query\s*\(\s*["'].*\+.*\+.*["']/g, + /exec\s*\(\s*[`"'].*\$\{.*\}.*[`"']/g, + /execute\s*\(\s*[`"'].*\$\{.*\}.*[`"']/g, + /\.query\s*\(\s*[`"'].*\+.*\+.*[`"']/g, + /SELECT.*FROM.*\+.*\+.*WHERE/gi, + /INSERT\s+INTO.*\+.*\+/gi, + /UPDATE\s+.*SET.*\+.*\+/gi, + /DELETE\s+FROM.*\+.*\+/gi, + ], + fileFilter: codeFileFilter, + exclude: [/\?.*\?/, /\$\d+/, /:%s/, /placeholder/, /parameterized/, /prepare/], + }, + { + id: 'INJ-002', + name: 'NoSQL Injection Vulnerability', + severity: 'high', + category: 'injection', + description: 'Potential NoSQL injection: user input used directly in MongoDB/NoSQL query object.', + patterns: [ + /find\s*\(\s*\{.*\$where\s*:/g, + /find\s*\(\s*\{.*\$eq\s*:\s*req\./g, + /find\s*\(\s*\{.*\$in\s*:\s*req\./g, + /findOne\s*\(\s*\{.*req\.(body|query|params)/g, + /find\s*\(\s*\{.*req\.(body|query|params)/g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'INJ-003', + name: 'Command Injection Vulnerability', + severity: 'critical', + category: 'injection', + description: 'Potential command injection: user input passed to shell execution functions.', + patterns: [ + /exec\s*\(\s*[`"'].*\$\{.*\}.*[`"']/g, + /execSync\s*\(\s*[`"'].*\$\{.*\}.*[`"']/g, + /spawn\s*\(\s*["'].*req\./g, + /child_process.*exec.*req\./g, + /eval\s*\(.*req\./g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'INJ-004', + name: 'Path Traversal Vulnerability', + severity: 'high', + category: 'injection', + description: 'Potential path traversal: user input used to construct file paths without sanitization.', + patterns: [ + /fs\.readFile\s*\(\s*.*req\.(body|query|params)/g, + /fs\.writeFile\s*\(\s*.*req\.(body|query|params)/g, + /res\.sendFile\s*\(\s*.*req\./g, + /readFileSync\s*\(\s*.*\+.*req\./g, + /path\.join\s*\(.*req\./g, + ], + fileFilter: codeFileFilter, + }, + // === XSS / CLIENT-SIDE VULNERABILITIES === + { + id: 'XSS-001', + name: 'DOM XSS: innerHTML with User Input', + severity: 'high', + category: 'client-side', + description: 'Potential DOM-based XSS: user input assigned to innerHTML without sanitization.', + patterns: [ + /innerHTML\s*=\s*.*(req\.|location\.|search|hash|href)/g, + /innerHTML\s*=\s*.*\$\{.*\}/g, + /document\.write\s*\(.*(req\.|location\.|search|hash|href)/g, + /outerHTML\s*=\s*.*(req\.|location\.|search|hash|href)/g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'XSS-002', + name: 'Dangerous eval() Usage', + severity: 'critical', + category: 'client-side', + description: 'eval() used with potentially user-controlled input. This is a severe security risk.', + patterns: [ + /eval\s*\(\s*.*(req\.|location\.|search|hash|href|cookie)/g, + /eval\s*\(\s*.*\$\{.*\}/g, + /new\s+Function\s*\(.*(req\.|location\.|search|hash|href)/g, + /setTimeout\s*\(\s*["'].*\+.*["']/g, + /setInterval\s*\(\s*["'].*\+.*["']/g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'XSS-003', + name: 'React dangerouslySetInnerHTML', + severity: 'medium', + category: 'client-side', + description: 'dangerouslySetInnerHTML used in React. Ensure content is sanitized before use.', + patterns: [ + /dangerouslySetInnerHTML\s*:/g, + ], + fileFilter: codeFileFilter, + }, + // === MISSING INPUT VALIDATION === + { + id: 'VAL-001', + name: 'Missing Input Validation on Express Route', + severity: 'medium', + category: 'validation', + description: 'Express route handler accesses req.body without visible validation. Add express-validator, joi, or zod.', + patterns: [ + /app\.(post|put|patch)\s*\(\s*["'].*["'].*\(\s*req\s*,\s*res\s*\).*\{\s*[^}]*req\.body(?!.*validate)/gs, + /app\.(post|put|patch)\s*\(\s*["'].*["'].*\(\s*req\s*,\s*res\s*\).*\{\s*[^}]{0,500}\}[^}]*\)/gs, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-002', + name: 'Direct User Input in Object Assignment', + severity: 'medium', + category: 'validation', + description: 'User input directly spread into objects without validation. This can lead to mass assignment vulnerabilities.', + patterns: [ + /\{\s*\.\.\.req\.body\s*\}/g, + /\.create\s*\(\s*req\.body\s*\)/g, + /\.insert\s*\(\s*req\.body\s*\)/g, + /Object\.assign\s*\(.*req\.body\)/g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-003', + name: 'Missing CORS Configuration', + severity: 'medium', + category: 'config', + description: 'CORS enabled with wildcard or no explicit origin restriction. This allows any website to make requests to your API.', + patterns: [ + /app\.use\s*\(\s*cors\s*\(\s*\)\s*\)/g, + /cors\s*\(\s*\{\s*origin\s*:\s*["']\*["']\s*\}\s*\)/g, + /Access-Control-Allow-Origin\s*:\s*\*/g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-004', + name: 'Insecure Cookie Configuration', + severity: 'high', + category: 'config', + description: 'Cookie set without secure flags (httpOnly, secure, sameSite). Vulnerable to XSS and MITM theft.', + patterns: [ + /res\.cookie\s*\(\s*[^,]+,\s*[^,]+\s*\)/g, + /cookie\s*\(\s*[^,]+,\s*[^,]+,\s*\{\s*[^}]*\}\s*\)/g, + ], + fileFilter: codeFileFilter, + exclude: [/httpOnly/, /secure.*true/, /sameSite/, /Secure/], + }, + { + id: 'VAL-005', + name: 'Insecure CORS with Credentials', + severity: 'high', + category: 'config', + description: 'CORS configured with credentials:true but without explicit origin restriction. Dangerous combination.', + patterns: [ + /credentials\s*:\s*true.*origin\s*:\s*["']\*["']/gs, + /origin\s*:\s*["']\*["'].*credentials\s*:\s*true/gs, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-006', + name: 'Debug Mode Enabled in Production Code', + severity: 'medium', + category: 'config', + description: 'DEBUG or development mode enabled in code. This can leak sensitive information.', + patterns: [ + /DEBUG\s*[:=]\s*true/gi, + /NODE_ENV\s*[:=]\s*["']development["']/gi, + /app\.use\s*\(\s*errorhandler\s*\)/gi, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-007', + name: 'Insecure HTTP Usage', + severity: 'medium', + category: 'config', + description: 'Hardcoded HTTP URLs instead of HTTPS for API calls or resources.', + patterns: [ + /http:\/\/(?!localhost|127\.0\.0\.1)/g, + ], + fileFilter: codeFileFilter, + exclude: [/http:\/\/localhost/, /http:\/\/127\.0\.0\.1/], + }, + { + id: 'VAL-008', + name: 'Insecure Randomness for Security Tokens', + severity: 'high', + category: 'config', + description: 'Math.random() used for token or ID generation. Use crypto.randomBytes() or crypto.randomUUID() instead.', + patterns: [ + /Math\.random\s*\(\s*\).*token/gi, + /Math\.random\s*\(\s*\).*id/gi, + /Math\.random\s*\(\s*\).*secret/gi, + /Math\.random\s*\(\s*\).*password/gi, + /Math\.random\s*\(\s*\).*session/gi, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-009', + name: 'Missing Rate Limiting on API Routes', + severity: 'medium', + category: 'validation', + description: 'No rate limiting middleware detected in Express/Fastify app. API routes are vulnerable to brute force and DoS.', + patterns: [ + /app\.(get|post|put|delete|patch)\s*\(\s*["']\/api\//g, + ], + fileFilter: codeFileFilter, + }, + { + id: 'VAL-010', + name: 'Insecure File Upload', + severity: 'high', + category: 'validation', + description: 'File upload without extension or type validation. Could allow executable uploads.', + patterns: [ + /multer\s*\(\s*\{\s*\}\s*\)/g, + /upload\s*\.single\s*\(.*\)\s*[^}]*[^a-zA-Z](?!.*\.ext|.*\.type|.*mimetype|.*extension)/g, + ], + fileFilter: codeFileFilter, + }, +]; + +export function getRulesByCategory(category: string): Rule[] { + return rules.filter((r) => r.category === category); +} + +export function getRulesBySeverity(severity: string): Rule[] { + return rules.filter((r) => r.severity === severity); +} diff --git a/src/scanner.test.ts b/src/scanner.test.ts new file mode 100644 index 0000000..3446e5b --- /dev/null +++ b/src/scanner.test.ts @@ -0,0 +1,138 @@ +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { writeFile, mkdir, rm } from 'node:fs/promises'; +import { join } from 'node:path'; +import { scan } from '../src/scanner.js'; + +const TEST_DIR = '/tmp/vibe-scan-test-' + Date.now(); + +async function setupTestFiles(): Promise { + await mkdir(TEST_DIR, { recursive: true }); + await mkdir(join(TEST_DIR, 'src'), { recursive: true }); + await mkdir(join(TEST_DIR, 'node_modules'), { recursive: true }); + + // File with exposed Stripe key + await writeFile( + join(TEST_DIR, 'src', 'payments.js'), + `const stripe = require('stripe')('sk_live_abcdefghijklmnopqrstuvwxyz');\n\nexport async function charge(amount) {\n return stripe.charges.create({ amount });\n}\n` + ); + + // File with SQL injection + await writeFile( + join(TEST_DIR, 'src', 'database.ts'), + `import { pool } from './db';\n\nexport async function getUser(id: string) {\n const result = await pool.query(\`SELECT * FROM users WHERE id = \${id}\`);\n return result.rows[0];\n}\n` + ); + + // File with exposed AWS key + await writeFile( + join(TEST_DIR, 'src', 'config.js'), + `module.exports = {\n awsAccessKey: 'AKIAIOSFODNN7EXAMPLE',\n region: 'us-east-1'\n};\n` + ); + + // File with XSS vulnerability + await writeFile( + join(TEST_DIR, 'src', 'render.js'), + `function displayUserInput(input) {\n document.getElementById('output').innerHTML = \`\${location.search}\`;\n}\n` + ); + + // Binary file (should be skipped) + await writeFile( + join(TEST_DIR, 'src', 'image.png'), + Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + ); + + // Clean file + await writeFile( + join(TEST_DIR, 'src', 'utils.ts'), + `export function add(a: number, b: number): number {\n return a + b;\n}\n` + ); + + // File in node_modules (should be ignored) + await writeFile( + join(TEST_DIR, 'node_modules', 'evil.js'), + `const key = 'sk_live_abcdefghijklmnopqrstuvwxyz';\n` + ); +} + +async function cleanup(): Promise { + await rm(TEST_DIR, { recursive: true, force: true }); +} + +describe('scanner', () => { + beforeAll(setupTestFiles); + afterAll(cleanup); + + it('should detect exposed Stripe secret key', async () => { + const result = await scan({ paths: [TEST_DIR] }); + const stripeFindings = result.findings.filter((f) => f.ruleId === 'SEC-001'); + expect(stripeFindings.length).toBeGreaterThanOrEqual(1); + expect(stripeFindings[0].severity).toBe('critical'); + expect(stripeFindings[0].file).toContain('payments.js'); + }); + + it('should detect SQL injection', async () => { + const result = await scan({ paths: [TEST_DIR] }); + const sqlFindings = result.findings.filter((f) => f.ruleId === 'INJ-001'); + expect(sqlFindings.length).toBeGreaterThanOrEqual(1); + expect(sqlFindings[0].severity).toBe('critical'); + expect(sqlFindings[0].file).toContain('database.ts'); + }); + + it('should detect exposed AWS key', async () => { + const result = await scan({ paths: [TEST_DIR] }); + const awsFindings = result.findings.filter((f) => f.ruleId === 'SEC-003'); + expect(awsFindings.length).toBeGreaterThanOrEqual(1); + expect(awsFindings[0].severity).toBe('critical'); + expect(awsFindings[0].file).toContain('config.js'); + }); + + it('should detect XSS vulnerability', async () => { + const result = await scan({ paths: [TEST_DIR] }); + const xssFindings = result.findings.filter((f) => f.ruleId === 'XSS-001'); + expect(xssFindings.length).toBeGreaterThanOrEqual(1); + expect(xssFindings[0].severity).toBe('high'); + expect(xssFindings[0].file).toContain('render.js'); + }); + + it('should not flag node_modules by default', async () => { + const result = await scan({ paths: [TEST_DIR] }); + const nodeModulesFindings = result.findings.filter((f) => f.file.includes('node_modules')); + expect(nodeModulesFindings.length).toBe(0); + }); + + it('should count files correctly', async () => { + const result = await scan({ paths: [TEST_DIR] }); + expect(result.filesScanned).toBe(5); // 5 files in src/ + expect(result.filesSkipped).toBeGreaterThanOrEqual(1); // node_modules files skipped + }); + + it('should filter by category', async () => { + const result = await scan({ paths: [TEST_DIR], categories: ['secret'] }); + const nonSecret = result.findings.filter((f) => f.category !== 'secret'); + expect(nonSecret.length).toBe(0); + }); + + it('should filter by severity', async () => { + const result = await scan({ paths: [TEST_DIR], severities: ['critical'] }); + const nonCritical = result.findings.filter((f) => f.severity !== 'critical'); + expect(nonCritical.length).toBe(0); + }); + + it('should return empty for clean directory', async () => { + const cleanDir = join(TEST_DIR, 'src'); + // Create a temp clean file + const cleanFile = join(cleanDir, 'clean-test.ts'); + await writeFile(cleanFile, `export const foo = 42;\n`); + const result = await scan({ paths: [cleanFile] }); + expect(result.findings.length).toBe(0); + }); + + it('should deduplicate findings', async () => { + // Write a file with the same pattern twice on the same line + const dupFile = join(TEST_DIR, 'src', 'dup.js'); + await writeFile(dupFile, `const a = 'sk_live_abcdefghijklmnopqrstuvwxyz'; const b = 'sk_live_abcdefghijklmnopqrstuvwxyz';\n`); + const result = await scan({ paths: [dupFile] }); + // Should still find at least 1, but not duplicate the same rule on same line + const stripeFindings = result.findings.filter((f) => f.ruleId === 'SEC-001'); + expect(stripeFindings.length).toBeLessThanOrEqual(2); + }); +}); diff --git a/src/scanner.ts b/src/scanner.ts new file mode 100644 index 0000000..961ff14 --- /dev/null +++ b/src/scanner.ts @@ -0,0 +1,288 @@ +import { Rule, rules } from './rules.js'; + +export interface Finding { + ruleId: string; + ruleName: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + category: string; + description: string; + file: string; + line: number; + column: number; + snippet: string; + match: string; +} + +export interface ScanOptions { + paths: string[]; + exclude?: string[]; + rules?: string[]; + categories?: string[]; + severities?: string[]; + maxFileSize?: number; + respectGitignore?: boolean; +} + +export interface ScanResult { + findings: Finding[]; + filesScanned: number; + filesSkipped: number; + rulesRun: number; + durationMs: number; +} + +// Node.js imports for file system operations +import { readFile, stat, readdir, access } from 'node:fs/promises'; +import { join, relative, dirname } from 'node:path'; +import { createReadStream } from 'node:fs'; +import { createInterface } from 'node:readline'; + +const DEFAULT_MAX_FILE_SIZE = 1024 * 1024; // 1MB +const TEXT_EXTENSIONS = new Set([ + 'js', 'ts', 'jsx', 'tsx', 'mjs', 'cjs', 'py', 'rb', 'go', 'java', 'php', 'cs', 'rs', 'c', 'cpp', 'h', 'hpp', 'swift', 'kt', 'scala', 'html', 'css', 'scss', 'sass', 'less', 'vue', 'svelte', 'json', 'yaml', 'yml', 'toml', 'xml', 'sql', 'md', 'sh', 'bash', 'zsh', 'ps1', 'Dockerfile', 'env', 'config', 'ini', 'properties', 'tf', 'hcl', 'graphql', 'prisma', 'sql', +]); + +const BINARY_EXTENSIONS = new Set([ + 'exe', 'dll', 'so', 'dylib', 'bin', 'dat', 'db', 'sqlite', 'sqlite3', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'ico', 'bmp', 'mp3', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'zip', 'tar', 'gz', 'bz2', '7z', 'rar', 'jar', 'war', 'ear', 'class', 'o', 'a', 'obj', 'pdb', 'wasm', 'map', +]); + +function isTextFile(filename: string): boolean { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + if (BINARY_EXTENSIONS.has(ext)) return false; + if (TEXT_EXTENSIONS.has(ext)) return true; + // Default to scanning unknown extensions + return true; +} + +async function shouldIgnore(file: string, exclude: string[], basePath: string): Promise { + const rel = relative(basePath, file); + for (const pattern of exclude) { + // Match exact segments or starts/ends with pattern + const segments = rel.split(/[\\/]/); + for (const seg of segments) { + if (seg === pattern || seg.startsWith(pattern + '.') || seg.startsWith(pattern + '-')) { + return true; + } + } + // Also check if the filename itself matches + const filename = file.split(/[\\/]/).pop() || ''; + if (filename === pattern) { + return true; + } + } + return false; +} + +async function hasGitignoreFile(path: string): Promise { + try { + await access(join(path, '.gitignore')); + return true; + } catch { + return false; + } +} + +async function parseGitignore(path: string): Promise { + try { + const content = await readFile(join(path, '.gitignore'), 'utf-8'); + return content + .split('\n') + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith('#')) + .map((l) => l.replace(/^\//, '').replace(/\/$/, '')); + } catch { + return []; + } +} + +async function getFilesToScan( + paths: string[], + exclude: string[], + respectGitignore: boolean, + maxFileSize: number +): Promise<{ files: string[]; skipped: number }> { + const files: string[] = []; + let skipped = 0; + + for (const p of paths) { + const s = await stat(p); + if (s.isFile()) { + if (!isTextFile(p)) { + skipped++; + continue; + } + if (s.size > maxFileSize) { + skipped++; + continue; + } + files.push(p); + continue; + } + + if (!s.isDirectory()) continue; + + let gitignorePatterns: string[] = []; + if (respectGitignore) { + gitignorePatterns = await parseGitignore(p); + } + const allExclude = [...exclude, ...gitignorePatterns, 'node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', '.vercel', '.output', 'vendor']; + + async function scanDir(dir: string) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const full = join(dir, entry.name); + if (await shouldIgnore(full, allExclude, p)) { + if (entry.isFile()) { + skipped++; + } + continue; + } + if (entry.isDirectory()) { + await scanDir(full); + } else if (entry.isFile()) { + if (!isTextFile(full)) { + skipped++; + continue; + } + const fstat = await stat(full); + if (fstat.size > maxFileSize) { + skipped++; + continue; + } + files.push(full); + } + } + } + + await scanDir(p); + } + + return { files, skipped }; +} + +function findLineColumn(content: string, offset: number): { line: number; column: number } { + let line = 1; + let col = 1; + for (let i = 0; i < offset && i < content.length; i++) { + if (content[i] === '\n') { + line++; + col = 1; + } else { + col++; + } + } + return { line, column: col }; +} + +function getSnippet(content: string, offset: number, matchLen: number): string { + const start = Math.max(0, offset - 40); + const end = Math.min(content.length, offset + matchLen + 40); + let snippet = content.slice(start, end); + snippet = snippet.replace(/\s+/g, ' ').trim(); + if (start > 0) snippet = '...' + snippet; + if (end < content.length) snippet = snippet + '...'; + return snippet; +} + +function applyRule(content: string, filename: string, rule: Rule): Finding[] { + const findings: Finding[] = []; + + if (rule.fileFilter && !rule.fileFilter(filename)) { + return findings; + } + + for (const pattern of rule.patterns) { + // Reset lastIndex for global regexes + pattern.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = pattern.exec(content)) !== null) { + const matchedText = match[0]; + + // Check exclude patterns + if (rule.exclude) { + let excluded = false; + for (const ex of rule.exclude) { + ex.lastIndex = 0; + if (ex.test(matchedText)) { + excluded = true; + break; + } + } + if (excluded) continue; + } + + const offset = match.index; + const { line, column } = findLineColumn(content, offset); + const snippet = getSnippet(content, offset, matchedText.length); + + findings.push({ + ruleId: rule.id, + ruleName: rule.name, + severity: rule.severity, + category: rule.category, + description: rule.description, + file: filename, + line, + column, + snippet, + match: matchedText.slice(0, 80), + }); + } + } + + return findings; +} + +export async function scan(options: ScanOptions): Promise { + const start = Date.now(); + const exclude = options.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', '.vercel', 'vendor', '.venv']; + const maxFileSize = options.maxFileSize || DEFAULT_MAX_FILE_SIZE; + const respectGitignore = options.respectGitignore !== false; + + let activeRules = rules; + if (options.rules && options.rules.length > 0) { + activeRules = rules.filter((r) => options.rules!.includes(r.id)); + } + if (options.categories && options.categories.length > 0) { + activeRules = activeRules.filter((r) => options.categories!.includes(r.category)); + } + if (options.severities && options.severities.length > 0) { + activeRules = activeRules.filter((r) => options.severities!.includes(r.severity)); + } + + let { files, skipped } = await getFilesToScan(options.paths, exclude, respectGitignore, maxFileSize); + const findings: Finding[] = []; + + for (const file of files) { + try { + const content = await readFile(file, 'utf-8'); + for (const rule of activeRules) { + const fileFindings = applyRule(content, file, rule); + findings.push(...fileFindings); + } + } catch { + skipped++; + } + } + + // Deduplicate: same file + line + ruleId + const seen = new Set(); + const deduped: Finding[] = []; + for (const f of findings) { + const key = `${f.file}:${f.line}:${f.ruleId}`; + if (!seen.has(key)) { + seen.add(key); + deduped.push(f); + } + } + + return { + findings: deduped, + filesScanned: files.length, + filesSkipped: skipped, + rulesRun: activeRules.length, + durationMs: Date.now() - start, + }; +} + +export { rules }; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b37b6b9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7a79215 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +});