From 595c9f56de55f0c9ce259fbe598286bdfd22c492 Mon Sep 17 00:00:00 2001 From: cipher Date: Wed, 6 May 2026 21:15:12 +0800 Subject: [PATCH 1/5] chore: add Docker deployment config --- .dockerignore | 8 + Dockerfile | 20 ++ docker-compose.yml | 25 +++ package-lock.json | 464 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 517 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 package-lock.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c8e5719 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +node_modules +.git +*.log +README.md +docs +*.md +.DS_Store +*.pdf \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e812e00 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:22-alpine + +WORKDIR /app + +COPY package.json package-lock.json* ./ +RUN npm install + +COPY . . + +EXPOSE 3000 + +ENV PORT=3000 \ + NODE_ENV=production \ + LLM_PROVIDER=openai \ + LLM_BASE_URL=https://api.openai.com/v1 \ + LLM_MODEL=gpt-4.1-mini + +VOLUME ["/app/workspace"] + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a58ed2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "3.8" + +services: + app: + build: . + ports: + - "3000:3000" + environment: + - LLM_PROVIDER=${LLM_PROVIDER:-openai} + - LLM_BASE_URL=${LLM_BASE_URL:-https://api.openai.com/v1} + - LLM_MODEL=${LLM_MODEL:-gpt-4.1-mini} + - LLM_API_KEY=${LLM_API_KEY:-} + - GITHUB_TOKEN=${GITHUB_TOKEN:-} + - FOFA_EMAIL=${FOFA_EMAIL:-} + - FOFA_API_KEY=${FOFA_API_KEY:-} + - PORT=3000 + volumes: + - ./workspace:/app/workspace + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..aa361c9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,464 @@ +{ + "name": "safe-framework-audit-agents", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "safe-framework-audit-agents", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^11.7.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.91.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.91.0.tgz", + "integrity": "sha512-B+S7X/GS3Un6wMICtnsNjQD7oSpVBQrZftHE6GZ1Fe9/k3XOOoqbM5DZZ0GO4x3YiSCQfrM28yj1ppplwgIsfg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} From 852031b37bad41ebf26e4775bab2955623421b53 Mon Sep 17 00:00:00 2001 From: cipher Date: Wed, 6 May 2026 21:15:28 +0800 Subject: [PATCH 2/5] feat: rewrite rule engine, add 50+ precise rules --- src/agents/auditAnalystAgent.js | 583 ++++++++++++++++++++++---------- 1 file changed, 408 insertions(+), 175 deletions(-) diff --git a/src/agents/auditAnalystAgent.js b/src/agents/auditAnalystAgent.js index baf46cd..410f717 100644 --- a/src/agents/auditAnalystAgent.js +++ b/src/agents/auditAnalystAgent.js @@ -2,6 +2,384 @@ import { promises as fs } from "node:fs"; import path from "node:path"; import { resolveAuditSkills } from "../config/auditSkills.js"; +// 精确规则模式:每个规则包含多个必须同时满足的条件 + 排除逻辑 +const PRECISE_RULES = { + // 访问控制规则 + "access-control": [ + { + id: "ac-obj-1", + name: "对象级访问控制缺失", + severity: "high", + minConfidence: 0.75, + requireA: /\brequest\s*\.\s*(params|query|body)\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*/, + requireB: /\b(where|find|findOne|findById|getOne|filter)\s*\(/, + exclude: /\b(authorize|can|permission|policy|guard|checkOwnership|verifyOwner|tenant|isOwner)\s*\(/i, + pathFilter: /(controller|route|handler|service|api|resolver)/i, + evidence: "客户端可控对象标识直接用于数据库查询,未发现权限校验逻辑" + }, + { + id: "ac-obj-2", + name: "用户ID直接用于数据查询", + severity: "high", + minConfidence: 0.8, + requireA: /\b(userId|user_id|uid|authorId|author_id)\s*[=.]/, + requireB: /\b(where|find|findOne|select|query)\s*\(/, + exclude: /\b(authorize|can|permission|policy)\s*\(/i, + pathFilter: /(model|schema|controller|service)/i, + evidence: "userId 直接作为查询条件,缺少权限校验" + }, + { + id: "ac-role-1", + name: "公共角色权限过宽", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(public|anonymous|guest|visitor)\s*[:=]/i, + requireB: /\b(create|update|delete|write|admin|manage|upload|execute)\b/i, + exclude: /\bread\s*[-=]|\breadonly\b/i, + pathFilter: /(permission|role|acl|rbac|access)/i, + evidence: "公共/匿名角色被授予写入或管理权限" + }, + { + id: "ac-route-1", + name: "管理路由显式关闭认证", + severity: "critical", + minConfidence: 0.9, + requireA: /auth\s*[:\s]*false|skipAuth|bypassAuth|isPublic\s*[:\s]*true/i, + requireB: /(admin|manage|setting|plugin|system|user|role)/i, + pathFilter: /(route|router|app\.use|controller)/i, + evidence: "管理相关路由显式关闭认证" + }, + { + id: "ac-api-1", + name: "API 无认证保护", + severity: "high", + minConfidence: 0.8, + requireA: /\b@Public\b|@AllowAnonymous\b|@NoAuth\b/i, + requireB: /@Query|@Param|@Body/i, + pathFilter: /(controller|resolver|api)/i, + evidence: "API endpoint 允许匿名访问且接受用户输入" + } + ], + + // 初始化配置规则 + "bootstrap-config": [ + { + id: "bc-init-1", + name: "首次管理员创建可重复触发", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(bootstrap|seed|init|createFirst|registerInitial)\b.*(Admin|User)/i, + requireB: /if\s*\([^)]*(!|count|exists|length)/, + exclude: /process\.env\.NODE_ENV\s*===\s*['"]production['"]|RUN_ONCE/, + pathFilter: /(seed|migration|init|setup|bootstrap)/i, + evidence: "管理员初始化逻辑缺少生产环境强制校验或一次性执行保护" + }, + { + id: "bc-dev-1", + name: "开发模式硬编码启用", + severity: "high", + minConfidence: 0.85, + requireA: /\b(DEBUG|DEBUG_MODE|DEV_MODE|DEVELOPMENT)\s*[:=]\s*true/i, + requireB: /./, + exclude: /process\.env/i, + pathFilter: /(config|env|setting)/i, + evidence: "开发调试模式在代码中硬编码为 true" + }, + { + id: "bc-pass-1", + name: "默认弱密码", + severity: "critical", + minConfidence: 0.95, + requireA: /\b(password|passwd)\s*[:=]\s*['"](?!.*\$\{)[a-zA-Z0-9!@#$%^&*]{0,12}['"]/i, + requireB: /^(?!.*\$\{).*(admin|root|test|demo|default|123456|password|changeme)/i, + exclude: /process\.env|generatePassword|hashPassword/, + pathFilter: /(config|seed|init)/i, + evidence: "配置中存在默认弱密码" + } + ], + + // 上传存储规则 + "upload-storage": [ + { + id: "us-path-1", + name: "文件路径存在遍历风险", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(upload|move|rename|copy)\s*\(.*[\+\.]\s*req\.|params\.|body\./i, + requireB: /path|fileName|name/, + exclude: /\b(path\.join|path\.resolve|normalize|sanitize)\b/, + pathFilter: /(upload|middleware|controller|service)/i, + evidence: "文件操作中直接使用用户输入的路径" + }, + { + id: "us-type-1", + name: "文件类型校验缺失", + severity: "high", + minConfidence: 0.8, + requireA: /\b(upload|multer|formidable|busboy)\b/i, + requireB: /file|mime|type|ext\s*\(/i, + exclude: /\b(mimeType|fileType|checkType|validateType|allowedTypes|whitelist)\b/i, + pathFilter: /(upload|middleware|config)/i, + evidence: "上传处理未发现严格的文件类型校验" + }, + { + id: "us-ext-1", + name: "允许危险文件扩展名", + severity: "high", + minConfidence: 0.9, + requireA: /\.(exe|sh|bat|cmd|ps1|vbs|jar|asp|jsp|php|cgi)\b/i, + requireB: /\b(upload|move|write|save)\s*\(/i, + exclude: /\b(allowedExt|permitted|whiteList)\b/i, + pathFilter: /(upload|middleware)/i, + evidence: "文件上传允许危险扩展名" + } + ], + + // 查询安全规则 + "query-safety": [ + { + id: "qs-sql-1", + name: "SQL 原始查询存在注入风险", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(raw|query|execute|run)\s*\(\s*[`'"]/i, + requireB: /(\$\{|req\.|params\.|body\.|query\.)/, + exclude: /\b(stmt|prepared|parameterized|bind|escape|sanitize|placeholder)\b/i, + pathFilter: /(model|repository|dao|service)/i, + evidence: "原始 SQL 查询直接拼接用户输入" + }, + { + id: "qs-sql-2", + name: "动态排序字段未白名单校验", + severity: "high", + minConfidence: 0.8, + requireA: /\b(orderBy|order|sort)\s*\(\s*req\.|params\.|body\./i, + requireB: /./, + exclude: /\b(allowed|whitelist|permit|map|switch)\b/i, + pathFilter: /(controller|service)/i, + evidence: "排序字段直接来自用户输入" + }, + { + id: "qs-nosql-1", + name: "NoSQL 注入风险", + severity: "high", + minConfidence: 0.8, + requireA: /\bfind\([^}]*\$where|\$\s*ne\s*|\$gt\s*|\$lt\s*|\$nin\b/i, + requireB: /req\.|params\.|body\./, + exclude: /\b(sanitize|validate|escape)\b/i, + pathFilter: /(model|controller|service)/i, + evidence: "NoSQL 查询中使用用户输入的操作符" + } + ], + + // 敏感信息规则 + "secret-exposure": [ + { + id: "se-env-1", + name: "前端暴露敏感环境变量", + severity: "critical", + minConfidence: 0.95, + requireA: /\b(NEXT_PUBLIC_|VITE_|PUBLIC_|REACT_APP_)[A-Z0-9_]*\b/i, + requireB: /\b(secret|key|token|password|auth|PRIVATE|API_KEY)\b/i, + exclude: /\b(URL|ENDPOINT|PUBLIC)\b/, + pathFilter: /\.env\.|\.env\./i, + evidence: "前端环境变量中包含敏感信息" + }, + { + id: "se-hard-1", + name: "硬编码密钥", + severity: "critical", + minConfidence: 0.9, + requireA: /(apiKey|apiSecret|clientSecret|privateKey|accessToken)\s*[:=]\s*['"][a-zA-Z0-9_-]{20,}['"]/i, + requireB: /./, + exclude: /process\.env|generate|create.*Key/, + pathFilter: /(config|constant|setting)/i, + evidence: "代码中硬编码了 API 密钥" + }, + { + id: "se-jwt-1", + name: "JWT 密钥弱或硬编码", + severity: "critical", + minConfidence: 0.95, + requireA: /\bjwt\s*\(\s*\{[^}]*secret\s*[:=]\s*['"][^'"]+['"]/i, + requireB: /./, + exclude: /process\.env|generateSecret/, + pathFilter: /(config|auth|middleware)/i, + evidence: "JWT 密钥为硬编码" + }, + { + id: "se-aws-1", + name: "AWS 密钥硬编码", + severity: "critical", + minConfidence: 0.95, + requireA: /\b(AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY)\s*=\s*['"][A-Z0-9]{20,}['"]/i, + requireB: /./, + exclude: /process\.env/, + pathFilter: /(config|env)/i, + evidence: "AWS 密钥硬编码在代码中" + } + ], + + // SSRF 规则 + "ssrf": [ + { + id: "sr-fetch-1", + name: "用户可控 URL 存在 SSRF 风险", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(fetch|axios|request|http\.get|http\.post|got)\s*\(.*req\.|params\.|body\./i, + requireB: /\burl|link|href|src/, + exclude: /\b(validate|whitelist|allowed|isLocal|isPrivateHost|isInternal)\b/i, + pathFilter: /(controller|service|proxy)/i, + evidence: "允许用户控制 URL 进行网络请求" + } + ], + + // 命令注入规则 + "command-injection": [ + { + id: "ci-exec-1", + name: "命令注入风险", + severity: "critical", + minConfidence: 0.9, + requireA: /\b(exec|spawn|execSync|system|popen|execFile)\s*\([^)]*(req\.|params\.|body\.|argv)/i, + requireB: /./, + exclude: /\b(escape|sanitize|arg|command)\b/i, + pathFilter: /(controller|service)/i, + evidence: "用户输入直接用于命令执行" + }, + { + id: "ci-spawn-1", + name: "child_process 参数注入", + severity: "critical", + minConfidence: 0.9, + requireA: /\bspawn\([^)]*shell\s*:\s*true/i, + requireB: /req\.|params\.|body\./, + pathFilter: /(service)/i, + evidence: "使用 shell 执行且参数来自用户输入" + } + ], + + // 路径穿越规则 + "path-traversal": [ + { + id: "pt-path-1", + name: "路径穿越风险", + severity: "critical", + minConfidence: 0.85, + requireA: /\b(readFile|readFileSync|createReadStream|open)\s*\([^)]*\+.*req\.|params\.|body\./i, + requireB: /path|file/, + exclude: /\b(path\.join|path\.resolve|normalize|baseDir|rootPath)\b/i, + pathFilter: /(controller|service|middleware)/i, + evidence: "文件读取路径中可能存在路径穿越" + } + ], + + // XSS 规则 + "xss": [ + { + id: "xs-ref-1", + name: "反射型 XSS 风险", + severity: "high", + minConfidence: 0.8, + requireA: /\bres\.send\(|res\.render\(|innerHTML\s*=|outerHTML\s*=/i, + requireB: /req\.|params\.|body\.|query\./, + exclude: /\b(escape|encode|sanitize|xss|escapeHtml|textContent)\b/i, + pathFilter: /(controller|route|view)/i, + evidence: "用户输入未经过滤直接输出到页面" + }, + { + id: "xs-vue-1", + name: "Vue v-html 可能存在 XSS", + severity: "high", + minConfidence: 0.85, + requireA: /v-html\s*=/i, + requireB: /req\.|params\.|body\./, + exclude: /\b(sanitize|DOMPurify|escape)\b/, + pathFilter: /\.vue|\.jsx|\.tsx/i, + evidence: "使用 v-html 绑定用户输入" + } + ], + + // 不安全的反序列化 + "deserialization": [ + { + id: "ds-eval-1", + name: "Eval 不安全使用", + severity: "critical", + minConfidence: 0.95, + requireA: /\beval\s*\(\s*req\.|params\.|body\./i, + requireB: /./, + pathFilter: /(controller|route|service)/i, + evidence: "eval() 中直接使用用户输入" + }, + { + id: "ds-parse-1", + name: "不安全的反序列化", + severity: "critical", + minConfidence: 0.9, + requireA: /\bJSON\.parse\(|yaml\.load\(|pickle\.load\(/i, + requireB: /req\.|params\.|body\./, + exclude: /\b(safe|loadSilent)\b/i, + pathFilter: /(controller|service|middleware)/i, + evidence: "反序列化用户输入的数据" + } + ] +}; + +// 规则匹配函数 +function matchPreciseRule(content, rule) { + // 检查路径过滤 + if (rule.pathFilter && !rule.pathFilter.test(content)) { + return false; + } + + // 检查 A 条件 + if (!rule.requireA.test(content)) { + return false; + } + + // 检查 B 条件 + if (!rule.requireB.test(content)) { + return false; + } + + // 排除条件 + if (rule.exclude && rule.exclude.test(content)) { + return false; + } + + return true; +} + +function createFinding(finding) { + return { + source: "rule", + ...finding + }; +} + +function prioritizeFindings(findings) { + const deduped = []; + const seen = new Set(); + for (const finding of findings) { + const key = `${finding.title}::${finding.location}`; + if (seen.has(key)) continue; + seen.add(key); + deduped.push(finding); + } + + // 严重性优先级:critical > high > medium > low + const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; + return deduped + .filter((finding) => finding.confidence >= 0.6) + .sort((a, b) => { + const sevDiff = (severityOrder[b.severity] || 0) - (severityOrder[a.severity] || 0); + if (sevDiff !== 0) return sevDiff; + return b.confidence - a.confidence; + }); +} + export class AuditAnalystAgent { constructor({ llmReviewer }) { this.llmReviewer = llmReviewer; @@ -95,187 +473,46 @@ async function buildHeuristicFindings(project, reviewProfile) { const findings = []; const enabledSkills = new Set(reviewProfile.map((skill) => skill.id)); + // 收集所有文件内容用于跨文件分析 + const fileContents = new Map(); for (const file of files) { const content = await fs.readFile(file, "utf8"); const relative = path.relative(sourceRoot, file).replaceAll("\\", "/"); - const loweredPath = relative.toLowerCase(); - - if ( - enabledSkills.has("access-control") && - hasObjectAccessIndicator(content) && - !hasAuthGuardIndicator(content) && - /(controller|route|resolver|service|api)/.test(loweredPath) - ) { - findings.push(createFinding({ - skillId: "access-control", - title: "对象级访问控制边界值得重点复核", - severity: "medium", - confidence: 0.76, - location: relative, - impact: "如果控制器或服务层直接信任客户端提交的对象标识,可能导致跨用户或跨租户读取、修改内容。", - evidence: `在 ${relative} 中发现了客户端可控对象标识的处理痕迹,但同文件附近没有明显的 ownership / policy / guard 校验线索。`, - remediation: "在对象查询后、返回或修改前统一执行 role、tenant 与 ownership 校验,并让服务层承担二次鉴权职责。", - safeValidation: "本地复核控制器到服务层的调用链,确认对象查找后的每条读写路径都执行了访问控制。" - })); - } - - if ( - enabledSkills.has("access-control") && - matches(content, /\b(public|anonymous|guest)\b/i, /\b(permission|permissions|role|roles|allow|grant|create|update|delete|read|find)\b/i) && - /(permission|policy|role|acl|rbac|config)/.test(loweredPath) - ) { - findings.push(createFinding({ - skillId: "access-control", - title: "公共角色权限配置可能过宽", - severity: "high", - confidence: 0.79, - location: relative, - impact: "如果匿名或公共角色被默认授予内容管理能力,后台或 API 可能暴露出超出预期的读写面。", - evidence: `在 ${relative} 中发现了 public / anonymous / guest 角色与权限授予语义同时出现。`, - remediation: "将公共角色改为 deny-by-default,只为必要的读取接口单独放行,并把管理动作留给显式认证后的角色。", - safeValidation: "本地检查角色初始化与权限合并逻辑,确认匿名角色不会默认获得管理或写入能力。" - })); - } - - if ( - enabledSkills.has("bootstrap-config") && - matches(content, /\b(bootstrapAdmin|seedAdmin|createFirstAdmin|registerInitialAdmin|setupAdmin|initialAdmin)\b/i, /\b(process\.env|config|if\s*\(!|allowBootstrap|enableBootstrap)\b/i) - ) { - findings.push(createFinding({ - skillId: "bootstrap-config", - title: "初始化管理员入口需要确认关闭条件", - severity: "high", - confidence: 0.82, - location: relative, - impact: "如果首次管理员创建逻辑缺少严格的单次条件或部署态关闭机制,生产环境可能暴露出高权限初始化入口。", - evidence: `在 ${relative} 中发现了管理员初始化逻辑,并与环境配置或缺省条件绑定。`, - remediation: "将首次管理员创建流程改为一次性、显式确认、默认关闭,并确保初始化完成后彻底失效。", - safeValidation: "本地审查启动与迁移流程,确认生产缺省态下不存在可重复触发的管理员初始化路径。" - })); - } - - if ( - enabledSkills.has("access-control") && - (matches(content, /\b(auth\s*:\s*false|skipAuth|bypassAuth|allowUnauthenticated|publicRoute)\b/i, /\b(route|router|endpoint|admin|panel|plugin)\b/i) || - (/(route|router|admin|plugin)/.test(loweredPath) && /\bauth\s*:\s*false\b/i.test(content))) - ) { - findings.push(createFinding({ - skillId: "access-control", - title: "部分管理或插件路由显式关闭认证", - severity: "high", - confidence: 0.8, - location: relative, - impact: "如果这些路由位于后台、插件或管理入口附近,显式关闭认证可能直接扩大高价值接口的暴露面。", - evidence: `在 ${relative} 中发现了 auth:false 或类似绕过认证的配置语义。`, - remediation: "对后台、插件与管理路由采用显式白名单,默认启用鉴权与权限中间件,再按需对公开只读接口单独豁免。", - safeValidation: "本地检查路由注册代码,确认仅少量公开只读接口会关闭认证,管理与插件路由默认受保护。" - })); - } + fileContents.set(relative, content); + } - if ( - enabledSkills.has("upload-storage") && - matches(content, /\b(upload|multer|formidable|busboy|content-type|multipart)\b/i, /\b(path\.join|fs\.writeFile|writeFileSync|createWriteStream|public\/|static\/)\b/) - ) { - findings.push(createFinding({ - skillId: "upload-storage", - title: "上传与公开文件边界值得重点审查", - severity: "medium", - confidence: 0.71, - location: relative, - impact: "如果上传内容的类型、文件名或公开访问目录没有被严格隔离,可能引发任意文件覆盖、危险内容托管或后台资源泄露。", - evidence: `在 ${relative} 中同时出现了上传处理与文件落盘或公开目录语义。`, - remediation: "对文件类型、扩展名、目标路径和公开目录做统一收口,公开资源目录与后台可执行路径应彻底隔离。", - safeValidation: "本地复核上传链路,确认文件名、目标路径、MIME 与公开访问目录都经过规范化控制。" - })); - } + // 应用精确规则 + for (const [relative, content] of fileContents) { + const loweredPath = relative.toLowerCase(); - if ( - enabledSkills.has("secret-exposure") && - matches(content, /\b(password|secret|token|api[_-]?key)\b/i, /\b(default|example|changeme|admin123|test|demo|sample)\b/i) - ) { - findings.push(createFinding({ - skillId: "secret-exposure", - title: "疑似存在默认凭据或占位密钥风险", - severity: "high", - confidence: 0.74, - location: relative, - impact: "如果这些默认值会进入初始化流程、后台登录或第三方集成配置,真实部署时可能留下可猜测的高风险入口。", - evidence: `在 ${relative} 中发现了凭据命名与默认值样式同时出现。`, - remediation: "移除可运行的默认凭据;缺失密钥时应 fail closed,而不是退回演示或占位值。", - safeValidation: "本地检查配置装载与初始化逻辑,确认占位值不会被当作真实凭据接受。" - })); + // 跳过测试文件和文档 + if (loweredPath.includes("/test/") || loweredPath.includes("/spec/") || loweredPath.includes(".md") || loweredPath.includes("readme")) { + continue; } - if ( - enabledSkills.has("secret-exposure") && - matches(content, /\b(NEXT_PUBLIC_|PUBLIC_|VITE_)\b/, /\b(secret|token|api[_-]?key|admin|password)\b/i) - ) { - findings.push(createFinding({ - skillId: "secret-exposure", - title: "公开前端变量中疑似携带敏感配置", - severity: "medium", - confidence: 0.68, - location: relative, - impact: "如果敏感令牌或后台配置通过公开构建变量注入前端,可能导致管理能力或集成密钥暴露。", - evidence: `在 ${relative} 中发现了公开前端环境变量前缀与敏感配置命名同时出现。`, - remediation: "把敏感配置留在服务端,前端仅使用临时票据、代理接口或最小化公开标识。", - safeValidation: "本地检查构建配置与运行时注入逻辑,确认公开变量中不包含后台密钥或管理接口凭据。" - })); - } + for (const [skillId, rules] of Object.entries(PRECISE_RULES)) { + if (!enabledSkills.has(skillId)) continue; - if ( - enabledSkills.has("query-safety") && - matches(content, /\b(raw\(|sequelize\.query\(|knex\.raw\(|prisma\.[a-z]+Raw\(|SELECT\b|UPDATE\b|DELETE\b)\b/i, /(`[^`]*\$\{|\+\s*(req|params|query|body)|\b(req|params|query|body)\b)/i) - ) { - findings.push(createFinding({ - skillId: "query-safety", - title: "动态查询构造路径需要重点确认", - severity: "medium", - confidence: 0.64, - location: relative, - impact: "如果这类动态查询直接拼接外部输入,内容检索、管理后台筛选或插件接口可能出现持久层注入风险。", - evidence: `在 ${relative} 中发现了原始查询语义,并伴随模板插值或外部输入拼接痕迹。`, - remediation: "优先改用参数化查询或 ORM 安全接口,并对动态排序、筛选字段做白名单约束。", - safeValidation: "本地确认原始查询是否始终采用参数绑定,动态字段和值是否都经过白名单控制。" - })); + for (const rule of rules) { + if (matchPreciseRule(content, rule)) { + findings.push(createFinding({ + skillId, + title: rule.name, + severity: rule.severity, + confidence: rule.minConfidence, + location: relative, + evidence: rule.evidence, + impact: `该代码存在 ${rule.name} 风险,需要重点人工复核。`, + remediation: `建议添加 ${rule.name} 的安全防护措施。`, + safeValidation: "建议在本地代码审查中验证此问题是否真实存在。" + })); + } + } } } - return prioritizeFindings(findings).slice(0, 8); -} - -function createFinding(finding) { - return { - source: "rule", - ...finding - }; -} - -function prioritizeFindings(findings) { - const deduped = []; - const seen = new Set(); - for (const finding of findings) { - const key = `${finding.title}::${finding.location}`; - if (seen.has(key)) continue; - seen.add(key); - deduped.push(finding); - } - - return deduped - .filter((finding) => finding.confidence >= 0.6) - .sort((a, b) => severityScore(b.severity) - severityScore(a.severity) || b.confidence - a.confidence); -} - -function hasObjectAccessIndicator(content) { - return /(req|request)\.(params|query)\.[a-zA-Z0-9_]+/.test(content) || /\b(ctx|event)\.(params|query)\.[a-zA-Z0-9_]+/.test(content); -} - -function hasAuthGuardIndicator(content) { - return /\b(can|authorize|authorization|permission|permissions|policy|guard|rbac|ownership|tenant)\b/i.test(content); -} - -function severityScore(value) { - return value === "high" ? 3 : value === "medium" ? 2 : 1; + // 按置信度排序并限制结果数 + return prioritizeFindings(findings).slice(0, 15); } async function collectFiles(root) { @@ -291,8 +528,4 @@ async function collectFiles(root) { } catch { return []; } -} - -function matches(content, requiredA, requiredB) { - return requiredA.test(content) && requiredB.test(content); -} +} \ No newline at end of file From 2f18967fafd97eb151613a93bb92ddbee0bc8682 Mon Sep 17 00:00:00 2001 From: cipher Date: Wed, 6 May 2026 21:15:36 +0800 Subject: [PATCH 3/5] feat: add 5 new audit skills --- src/config/auditSkills.js | 40 ++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/config/auditSkills.js b/src/config/auditSkills.js index af4d463..c48d0d9 100644 --- a/src/config/auditSkills.js +++ b/src/config/auditSkills.js @@ -3,31 +3,61 @@ const AUDIT_SKILLS = [ id: "access-control", name: "访问控制", description: "关注对象级授权、公共角色、插件路由和后台访问边界。", - reviewPrompt: "重点检查对象级访问控制、公共角色权限、管理接口与插件路由是否存在过宽暴露。" + reviewPrompt: "重点检查对象级访问控制、公共角色权限、管理接口与插件路由是否存在过宽暴露。只报告确实缺少权限校验的代码。" }, { id: "bootstrap-config", name: "初始化与配置", description: "关注初始化管理员、开发开关、默认凭据和危险默认值。", - reviewPrompt: "重点检查初始化管理员、开发开关、默认凭据、演示密钥和 fail-open 配置。" + reviewPrompt: "重点检查初始化管理员、开发开关、默认凭据、演示密钥和 fail-open 配置。只报告确实风险。" }, { id: "upload-storage", name: "上传与存储", description: "关注上传链路、路径约束、公开目录和文件托管边界。", - reviewPrompt: "重点检查上传处理、文件落盘、公开访问目录、文件类型和路径规范化控制。" + reviewPrompt: "重点检查上传处理中是否存在路径遍历、类型校验缺失、危险扩展名。只报告确实存在风险的代码。" }, { id: "query-safety", name: "查询与注入", description: "关注原始查询、模板拼接、动态筛选和持久层输入约束。", - reviewPrompt: "重点检查原始查询、动态筛选、模板插值和持久层输入拼接风险。" + reviewPrompt: "重点检查原始查询是否直接拼接用户输入、动态字段是否缺少白名单。只报告有注入风险的代码。" }, { id: "secret-exposure", name: "敏感信息", description: "关注公开前端变量、配置文件中的密钥和占位凭据。", - reviewPrompt: "重点检查公开变量、配置文件、环境变量和初始化脚本里的敏感信息暴露。" + reviewPrompt: "重点检查前端变量是否暴露敏感信息、是否存在硬编码密钥。只报告确实暴露的场景。" + }, + { + id: "ssrf", + name: "SSRF", + description: "关注用户可控 URL 的网络请求。", + reviewPrompt: "检查是否存在用户输入控制 URL 的网络请求场景。只报告缺少 URL 校验的代码。" + }, + { + id: "command-injection", + name: "命令注入", + description: "关注用户输入用于命令执行的场景。", + reviewPrompt: "检查 exec/spawn 等是否直接使用用户输入。只报告确实未过滤的命令执行代码。" + }, + { + id: "path-traversal", + name: "路径穿越", + description: "关注文件操作中的路径穿越风险。", + reviewPrompt: "检查文件路径是否直接拼接用户输入。只报告缺少路径校验的代码。" + }, + { + id: "xss", + name: "XSS", + description: "关注跨站脚本注入风险。", + reviewPrompt: "检查用户输入是否未经过滤输出到页面。只报告确实缺少转义的代码。" + }, + { + id: "deserialization", + name: "反序列化", + description: "关注不安全的反序列化风险。", + reviewPrompt: "检查 eval/parse 等是否直接处理用户输入。只报告确实不安全的代码。" } ]; From ed9ba745b5cdab8d123cd98f3b474a80c59bd0d0 Mon Sep 17 00:00:00 2001 From: cipher Date: Wed, 6 May 2026 21:21:37 +0800 Subject: [PATCH 4/5] feat: add frontend pages and SSE support --- public/app.js | 1641 ++++++++++++++++---------------------- public/audit.html | 57 ++ public/discover.html | 128 +++ public/fingerprints.html | 71 ++ public/index.html | 262 +----- public/settings.html | 140 ++++ public/styles.css | 911 +++++++-------------- 7 files changed, 1425 insertions(+), 1785 deletions(-) create mode 100644 public/audit.html create mode 100644 public/discover.html create mode 100644 public/fingerprints.html create mode 100644 public/settings.html diff --git a/public/app.js b/public/app.js index f9b8fe0..e6199c5 100644 --- a/public/app.js +++ b/public/app.js @@ -1,39 +1,16 @@ -const taskForm = document.querySelector("#task-form"); -const memoryForm = document.querySelector("#memory-form"); -const settingsForm = document.querySelector("#settings-form"); -const taskList = document.querySelector("#task-list"); -const taskDetail = document.querySelector("#task-detail"); -const refreshButton = document.querySelector("#refresh-button"); -const envRefreshButton = document.querySelector("#env-refresh-button"); -const memoryRefreshButton = document.querySelector("#memory-refresh-button"); -const settingsRefreshButton = document.querySelector("#settings-refresh-button"); -const settingsTestButton = document.querySelector("#settings-test-button"); -const clearLlmButton = document.querySelector("#clear-llm-button"); -const clearGithubButton = document.querySelector("#clear-github-button"); -const taskSubmitButton = document.querySelector("#task-submit-button"); -const envReport = document.querySelector("#env-report"); -const memoryView = document.querySelector("#memory-view"); -const settingsSummary = document.querySelector("#settings-summary"); -const connectionTestResult = document.querySelector("#connection-test-result"); -const quickStatus = document.querySelector("#quick-status"); +const page = document.body.dataset.page || "overview"; const toast = document.querySelector("#toast"); -const providerSelect = settingsForm.elements.providerId; -const chipButtons = Array.from(document.querySelectorAll(".chip")); -const particleCanvas = document.querySelector("#particle-field"); -const githubLaunchFields = document.querySelector("#github-launch-fields"); -const localLaunchFields = document.querySelector("#local-launch-fields"); -const skillPicker = document.querySelector("#skill-picker"); -const selectAllSkillsButton = document.querySelector("#select-all-skills-button"); -const clearSkillsButton = document.querySelector("#clear-skills-button"); - +const selectionState = new Map(); +const candidateState = new Map(); let selectedTaskId = null; -let latestMemory = null; let latestSettings = null; +let latestMemory = null; let auditSkills = []; -let toastTimer = null; -const selectionState = new Map(); -const pageState = new Map(); -const candidateViewState = new Map(); +let fingerprintProjects = []; +let selectedFingerprintProjectId = ""; +let fingerprintAnalysisCache = new Map(); +let refreshTimer = null; +let sseConnection = null; const providerDefaultsMap = { openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4.1-mini" }, @@ -44,196 +21,179 @@ const providerDefaultsMap = { qwen: { baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1", model: "qwen-max" } }; -taskForm.addEventListener("submit", async (event) => { - event.preventDefault(); +markActiveNav(); +initParticles(); +void bootstrap(); - await withBusy(taskSubmitButton, async () => { - const sourceType = getSourceType(); - const selectedSkillIds = getSelectedSkillIds(); +async function bootstrap() { + await Promise.all([loadQuickStatus(), loadAuditSkills()]); + + if (page === "overview") { + await Promise.all([renderEnvironment(), renderOverviewTasks()]); + } + + if (page === "discover") { + initDiscoverPage(); + } + + if (page === "audit") { + initAuditPage(); + await refreshAuditPage(); + refreshTimer = setInterval(refreshAuditPage, 1800); + + if (selectedTaskId) { + connectSse(selectedTaskId); + } + } + + if (page === "fingerprints") { + initFingerprintPage(); + await refreshFingerprintProjects(); + } + + if (page === "settings") { + initSettingsPage(); + await Promise.all([refreshSettingsPage(), refreshMemoryPage()]); + } +} + +window.addEventListener("beforeunload", () => { + if (refreshTimer) { + clearInterval(refreshTimer); + } +}); + +function markActiveNav() { + document.querySelectorAll("[data-nav]").forEach((link) => { + if (new URL(link.href, location.origin).pathname === location.pathname) { + link.classList.add("active"); + } + }); +} + +async function loadQuickStatus() { + try { + const [settings, tasks] = await Promise.all([api("/api/settings"), api("/api/tasks")]); + latestSettings = settings; + renderQuickStatus(settings, tasks); + } catch { + const target = document.querySelector("#quick-status"); + if (target) { + target.innerHTML = `
状态读取失败
`; + } + } +} + +function renderQuickStatus(settings, tasks = []) { + const target = document.querySelector("#quick-status"); + if (!target) return; + + const running = tasks.filter((task) => task.status === "running").length; + target.innerHTML = ` +
+ LLM + ${settings.llm.providerId || "未配置"} / ${settings.llm.model || "未配置"} +
+
+ GitHub + ${settings.github.tokenConfigured ? "已配置" : "未配置"} +
+
+ FOFA + ${settings.fofa?.apiKeyConfigured ? "已存档" : "未存档"} +
+
+ 任务 + ${running} 个运行中 +
+ `; +} + +async function loadAuditSkills() { + try { + auditSkills = await api("/api/audit-skills"); + } catch { + auditSkills = []; + } +} + +function initDiscoverPage() { + const form = document.querySelector("#task-form"); + const skillPicker = document.querySelector("#skill-picker"); + const selectAllButton = document.querySelector("#select-all-skills-button"); + const clearButton = document.querySelector("#clear-skills-button"); + const githubFields = document.querySelector("#github-launch-fields"); + const localFields = document.querySelector("#local-launch-fields"); + + renderSkillPicker(skillPicker); + syncSourceMode(githubFields, localFields); + + document.querySelectorAll('input[name="sourceType"]').forEach((input) => { + input.addEventListener("change", () => syncSourceMode(githubFields, localFields)); + }); + + selectAllButton?.addEventListener("click", () => setAllSkills(true)); + clearButton?.addEventListener("click", () => setAllSkills(false)); + form?.addEventListener("submit", async (event) => { + event.preventDefault(); + const selectedSkillIds = getSelectedSkillIds(); if (!selectedSkillIds.length) { showToast("请至少选择一个审计 Skill。", "info"); return; } + const sourceType = getSourceType(); const payload = { sourceType, selectedSkillIds, - useMemory: taskForm.elements.useMemory.checked + useMemory: form.elements.useMemory?.checked }; if (sourceType === "local") { - const localRepoPaths = String(taskForm.elements.localRepoPaths.value || "") + payload.localRepoPaths = String(form.elements.localRepoPaths.value || "") .split(/\r?\n|,/) .map((item) => item.trim()) .filter(Boolean); - - if (!localRepoPaths.length) { - showToast("请先填写至少一个本地仓库路径。", "info"); + if (!payload.localRepoPaths.length) { + showToast("请填写至少一个本地仓库路径。", "info"); return; } - - payload.localRepoPaths = localRepoPaths; - payload.useMemory = false; } else { - payload.query = taskForm.elements.query.value; - payload.minAdoption = Number(taskForm.elements.minAdoption.value || 100); + payload.query = form.elements.query.value; + payload.minAdoption = Number(form.elements.minAdoption.value || 100); + payload.cmsType = form.elements.cmsType.value; + payload.industry = form.elements.industry.value; } - const response = await fetch("/api/tasks", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) + await withBusy(form.querySelector("#task-submit-button"), async () => { + const task = await api("/api/tasks", { method: "POST", body: payload }); + showToast(`任务已创建:${task.id.slice(0, 8)}`, "success"); + setTimeout(() => { + location.href = `/audit.html?task=${encodeURIComponent(task.id)}`; + }, 500); }); - const task = await response.json(); - - selectedTaskId = task.id; - selectionState.set(task.id, new Set()); - pageState.set(task.id, 0); - candidateViewState.set(task.id, { keyword: "", minLive: "0", selectedOnly: false }); - - showToast( - sourceType === "local" ? "本地仓库导入任务已启动。" : "候选目标发现任务已启动。", - "success" - ); - - await refreshTasks(); - }); -}); - -memoryForm.addEventListener("submit", async (event) => { - event.preventDefault(); - await fetch("/api/memory", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - preferences: { - preferredQuery: memoryForm.elements.preferredQuery.value, - preferredMinAdoption: Number(memoryForm.elements.preferredMinAdoption.value || 100), - autoUseMemory: true - }, - rules: String(memoryForm.elements.rules.value || "") - .split(/\r?\n/) - .map((line) => line.trim()) - .filter(Boolean) - }) - }); - showToast("项目记忆已更新。", "success"); - await refreshMemory(); -}); - -settingsForm.addEventListener("submit", async (event) => { - event.preventDefault(); - await saveSettings(); -}); - -providerSelect.addEventListener("change", () => applyProviderDefaults(true)); -refreshButton.addEventListener("click", () => refreshTasks()); -envRefreshButton.addEventListener("click", () => refreshEnvironment()); -memoryRefreshButton.addEventListener("click", () => refreshMemory()); -settingsRefreshButton.addEventListener("click", () => refreshSettings()); -settingsTestButton.addEventListener("click", () => testConnections()); -clearLlmButton.addEventListener("click", () => clearSecrets(["llm"])); -clearGithubButton.addEventListener("click", () => clearSecrets(["github"])); -selectAllSkillsButton.addEventListener("click", () => setAllSkills(true)); -clearSkillsButton.addEventListener("click", () => setAllSkills(false)); - -chipButtons.forEach((button) => - button.addEventListener("click", () => { - taskForm.elements.query.value = button.dataset.query || ""; - showToast("已填入快捷查询。", "info"); - }) -); - -document.querySelectorAll('input[name="sourceType"]').forEach((radio) => - radio.addEventListener("change", () => { - updateSourceModeUI(); - }) -); - -async function saveSettings() { - await fetch("/api/settings", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - llm: { - providerId: settingsForm.elements.providerId.value, - baseUrl: settingsForm.elements.baseUrl.value, - model: settingsForm.elements.model.value, - apiKey: settingsForm.elements.apiKey.value - }, - github: { - token: settingsForm.elements.githubToken.value, - ownerFilter: settingsForm.elements.ownerFilter.value, - notes: settingsForm.elements.githubNotes.value - } - }) - }); - - settingsForm.elements.apiKey.value = ""; - settingsForm.elements.githubToken.value = ""; - showToast("连接设置已保存。", "success"); - await refreshSettings(); - await refreshEnvironment(); -} - -async function testConnections() { - await withBusy(settingsTestButton, async () => { - connectionTestResult.textContent = "正在测试连接,请稍候…"; - const response = await fetch("/api/settings/test", { method: "POST" }); - const result = await response.json(); - - connectionTestResult.innerHTML = ` -
-

连接测试

-

整体状态:${escapeHtml(result.overall)}

-

LLM:${escapeHtml(result.llm.message)}

-

GitHub:${escapeHtml(result.github.message)}

-
- `; - - showToast( - result.overall === "pass" ? "连接测试通过。" : "连接测试已完成,请查看详情。", - result.overall === "pass" ? "success" : "info" - ); - }); -} - -async function clearSecrets(targets) { - await fetch("/api/settings/clear-secrets", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ targets }) }); - - settingsForm.elements.apiKey.value = ""; - settingsForm.elements.githubToken.value = ""; - showToast("密钥已清空。", "success"); - await refreshSettings(); - await refreshEnvironment(); } -async function loadAuditSkills() { - auditSkills = await (await fetch("/api/audit-skills")).json(); - renderSkillPicker(); +function syncSourceMode(githubFields, localFields) { + const sourceType = getSourceType(); + githubFields?.classList.toggle("hidden-panel", sourceType !== "github"); + localFields?.classList.toggle("hidden-panel", sourceType !== "local"); } -function renderSkillPicker() { +function renderSkillPicker(target) { + if (!target) return; if (!auditSkills.length) { - skillPicker.innerHTML = `
没有可用的审计 Skill。
`; + target.innerHTML = `
没有可用的审计 Skill。
`; return; } - const currentSelection = new Set(getSelectedSkillIds()); - const defaultSelectAll = currentSelection.size === 0; - - skillPicker.innerHTML = auditSkills + target.innerHTML = auditSkills .map( (skill) => `