fullshield 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/LICENSE +21 -0
  3. package/README.md +155 -1
  4. package/dist/agent.js +5737 -0
  5. package/dist/cli/index.js +8540 -0
  6. package/dist/compliance.js +126 -0
  7. package/dist/confirm.js +146 -0
  8. package/dist/dashboard.js +121 -0
  9. package/dist/iac.js +84 -0
  10. package/dist/incremental.js +13 -0
  11. package/dist/index.d.ts +334 -0
  12. package/dist/index.js +6180 -0
  13. package/dist/llm.js +105 -0
  14. package/dist/mcp.js +5831 -0
  15. package/dist/runtime/auto.js +1163 -0
  16. package/dist/runtime/express.js +229 -0
  17. package/dist/runtime/fastify.js +241 -0
  18. package/dist/runtime/index.js +216 -0
  19. package/dist/runtime/rasp/loader.js +14 -0
  20. package/dist/runtime/rasp.js +1339 -0
  21. package/dist/runtime/register.js +1171 -0
  22. package/dist/supply.js +228 -0
  23. package/dist/threat.js +63 -0
  24. package/dist/workspaces.js +23 -0
  25. package/package.json +85 -8
  26. package/rules/CONFIG-BODYLIMIT-001.md +9 -0
  27. package/rules/CONFIG-CORS-001.md +9 -0
  28. package/rules/CONFIG-DOCKER-ENV-001.md +9 -0
  29. package/rules/CONFIG-DOCKER-LATEST-001.md +9 -0
  30. package/rules/CONFIG-DOCKER-ROOT-001.md +9 -0
  31. package/rules/CONFIG-ERRSTACK-001.md +9 -0
  32. package/rules/CONFIG-HELMET-001.md +9 -0
  33. package/rules/CONFIG-INSTALLSCRIPT-001.md +9 -0
  34. package/rules/CONFIG-RATELIMIT-001.md +9 -0
  35. package/rules/DEPS-DEPRECATED-001.md +9 -0
  36. package/rules/DEPS-NOLOCK-001.md +9 -0
  37. package/rules/DEPS-TYPOSQUAT-001.md +9 -0
  38. package/rules/DEPS-VULN-001.md +9 -0
  39. package/rules/README.md +60 -0
  40. package/rules/SAST-AUTH-001.md +9 -0
  41. package/rules/SAST-CMDI-001.md +9 -0
  42. package/rules/SAST-CRYPTO-001.md +9 -0
  43. package/rules/SAST-EVAL-001.md +9 -0
  44. package/rules/SAST-JWT-001.md +9 -0
  45. package/rules/SAST-NOSQLI-001.md +9 -0
  46. package/rules/SAST-PATH-001.md +9 -0
  47. package/rules/SAST-PROTO-001.md +9 -0
  48. package/rules/SAST-REDIRECT-001.md +9 -0
  49. package/rules/SAST-SQLI-001.md +9 -0
  50. package/rules/SAST-SSRF-001.md +9 -0
  51. package/rules/SAST-XSS-001.md +9 -0
  52. package/rules/SECRET-AIKEY-001.md +9 -0
  53. package/rules/SECRET-AWS-001.md +9 -0
  54. package/rules/SECRET-DBURL-001.md +9 -0
  55. package/rules/SECRET-ENTROPY-001.md +9 -0
  56. package/rules/SECRET-ENVFILE-001.md +9 -0
  57. package/rules/SECRET-GITHUB-001.md +9 -0
  58. package/rules/SECRET-GOOGLE-001.md +9 -0
  59. package/rules/SECRET-JWT-001.md +9 -0
  60. package/rules/SECRET-PRIVKEY-001.md +9 -0
  61. package/rules/SECRET-SLACK-001.md +9 -0
  62. package/rules/SECRET-STRIPE-001.md +9 -0
  63. package/templates/fullshield.config.json +7 -0
  64. package/templates/github-watch.yml +15 -0
  65. package/templates/github-workflow.yml +19 -0
  66. package/templates/gitlab-ci.yml +11 -0
  67. package/templates/pre-commit.sh +11 -0
package/dist/supply.js ADDED
@@ -0,0 +1,228 @@
1
+ import { existsSync, statSync, readFileSync, readdirSync } from 'fs';
2
+ import { join, relative } from 'path';
3
+ import { createHash } from 'crypto';
4
+
5
+ // src/supply/index.ts
6
+ function makeFindingId(ruleId, file, snippet) {
7
+ const normalized = (snippet ?? "").replace(/\s+/g, " ").trim();
8
+ return createHash("sha256").update(`${ruleId}\0${file ?? ""}\0${normalized}`).digest("hex").slice(0, 16);
9
+ }
10
+
11
+ // src/supply/index.ts
12
+ var REGISTRY_HOST = "registry.npmjs.org";
13
+ var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
14
+ function assertRegistryUrl(url) {
15
+ let parsed;
16
+ try {
17
+ parsed = new URL(url);
18
+ } catch {
19
+ throw new Error("supply: registry URL must be https://registry.npmjs.org/...");
20
+ }
21
+ if (parsed.protocol !== "https:" || parsed.hostname !== REGISTRY_HOST) {
22
+ throw new Error(`supply: only ${REGISTRY_HOST} is allowed (got ${parsed.hostname || url})`);
23
+ }
24
+ }
25
+ function packageNameFromSpec(spec) {
26
+ if (spec.startsWith("@")) {
27
+ const slash = spec.indexOf("/");
28
+ if (slash < 0) return spec;
29
+ const scope = spec.slice(0, slash);
30
+ const rest = spec.slice(slash + 1);
31
+ const name = rest.includes("@") ? rest.slice(0, rest.indexOf("@")) : rest;
32
+ return `${scope}/${name}`;
33
+ }
34
+ const at = spec.indexOf("@");
35
+ return at > 0 ? spec.slice(0, at) : spec;
36
+ }
37
+ function finding(ruleId, title, message, file, severity = "high") {
38
+ return {
39
+ id: makeFindingId(ruleId, file, message),
40
+ ruleId,
41
+ module: "deps",
42
+ title,
43
+ severity,
44
+ confidence: "high",
45
+ file,
46
+ message,
47
+ remediation: "Do not install this package until the behavior is reviewed. Use --ignore-scripts."
48
+ };
49
+ }
50
+ function walkJs(dir, out = []) {
51
+ if (!existsSync(dir)) return out;
52
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
53
+ const full = join(dir, entry.name);
54
+ if (entry.isDirectory()) {
55
+ if (entry.name === "node_modules") continue;
56
+ walkJs(full, out);
57
+ } else if (entry.isFile() && /\.(js|cjs|mjs|ts)$/.test(entry.name)) {
58
+ out.push(full);
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+ function readJson(path) {
64
+ return JSON.parse(readFileSync(path, "utf8"));
65
+ }
66
+ function installScriptKeys(scripts) {
67
+ if (!scripts) return [];
68
+ return INSTALL_SCRIPT_KEYS.filter((k) => typeof scripts[k] === "string");
69
+ }
70
+ function analyzePackageDir(dir) {
71
+ const findings = [];
72
+ const manifestPath = join(dir, "package.json");
73
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) return findings;
74
+ const pkg = readJson(manifestPath);
75
+ const scripts = pkg.scripts ?? {};
76
+ const keys = installScriptKeys(scripts);
77
+ if (keys.length > 0) {
78
+ findings.push(
79
+ finding(
80
+ "SUPPLY-INSTALLSCRIPT-001",
81
+ "Package declares an install script",
82
+ `install scripts: ${keys.join(", ")}`,
83
+ "package.json"
84
+ )
85
+ );
86
+ }
87
+ const sources = walkJs(dir).map((file) => ({
88
+ file: relative(dir, file) || file,
89
+ text: readFileSync(file, "utf8")
90
+ }));
91
+ const readsNpmrc = sources.some((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
92
+ const hasNet = sources.some((s) => /https\.request|http\.request|fetch\s*\(|axios\./.test(s.text));
93
+ if (readsNpmrc && hasNet) {
94
+ const hit = sources.find((s) => s.text.includes(".npmrc") || s.text.includes(".ssh"));
95
+ findings.push(
96
+ finding(
97
+ "SUPPLY-NPMRC-NET-001",
98
+ "Install-time code reads npm/ssh credentials and uses the network",
99
+ "static: credential file read combined with http(s)/fetch",
100
+ hit?.file,
101
+ "critical"
102
+ )
103
+ );
104
+ }
105
+ for (const src of sources) {
106
+ if (/eval\s*\(\s*(Buffer|atob|fromCharCode|decodeURIComponent)/.test(src.text)) {
107
+ findings.push(
108
+ finding(
109
+ "SUPPLY-OBFUSCATE-001",
110
+ "Package evals a decoded/obfuscated string",
111
+ "static: eval of decoded payload",
112
+ src.file
113
+ )
114
+ );
115
+ }
116
+ }
117
+ return findings;
118
+ }
119
+ function diffPackageVersions(oldDir, newDir) {
120
+ const oldManifest = existsSync(join(oldDir, "package.json")) ? readJson(join(oldDir, "package.json")) : {};
121
+ const nextManifest = existsSync(join(newDir, "package.json")) ? readJson(join(newDir, "package.json")) : {};
122
+ const oldScripts = oldManifest.scripts ?? {};
123
+ const nextScripts = nextManifest.scripts ?? {};
124
+ const added = INSTALL_SCRIPT_KEYS.filter((k) => typeof nextScripts[k] === "string" && oldScripts[k] === void 0);
125
+ const oldSrc = walkJs(oldDir).map((f) => readFileSync(f, "utf8")).join("\n");
126
+ const newSrc = walkJs(newDir).map((f) => readFileSync(f, "utf8")).join("\n");
127
+ const newChild = /child_process/.test(newSrc) && !/child_process/.test(oldSrc);
128
+ const newNet = /https?\.request|fetch\s*\(/.test(newSrc) && !/https?\.request|fetch\s*\(/.test(oldSrc);
129
+ if (added.length === 0 && !newChild && !newNet) return [];
130
+ return [
131
+ finding(
132
+ "SUPPLY-BEHAVIOR-001",
133
+ "Package version adds install-script or process/network behavior",
134
+ `added scripts=${added.join(",") || "none"} child_process=${newChild} network=${newNet}`,
135
+ "package.json"
136
+ )
137
+ ];
138
+ }
139
+ function checkCooldown(packument, version, minHours = 72, nowMs = Date.now()) {
140
+ const stamp = packument.time?.[version];
141
+ if (!stamp) return [];
142
+ const published = Date.parse(stamp);
143
+ if (Number.isNaN(published)) return [];
144
+ const ageHours = (nowMs - published) / 36e5;
145
+ if (ageHours >= minHours) return [];
146
+ return [
147
+ finding(
148
+ "SUPPLY-COOLDOWN-001",
149
+ "Package version is younger than the release cooldown",
150
+ `${version} published ${ageHours.toFixed(1)}h ago (min ${minHours}h)`,
151
+ void 0,
152
+ "medium"
153
+ )
154
+ ];
155
+ }
156
+ function checkLockfileIntegrity(lock) {
157
+ const findings = [];
158
+ for (const [name, entry] of Object.entries(lock.packages ?? {})) {
159
+ if (name === "") continue;
160
+ const resolved = entry.resolved;
161
+ if (!resolved) continue;
162
+ try {
163
+ const host = new URL(resolved).hostname;
164
+ if (host !== REGISTRY_HOST) {
165
+ findings.push(
166
+ finding(
167
+ "SUPPLY-LOCKURL-001",
168
+ "Lockfile entry resolves from a non-npm registry host",
169
+ `${name} resolved host ${host}`,
170
+ "package-lock.json"
171
+ )
172
+ );
173
+ }
174
+ } catch {
175
+ findings.push(
176
+ finding(
177
+ "SUPPLY-LOCKURL-001",
178
+ "Lockfile entry has a non-URL resolved field",
179
+ `${name} resolved ${resolved}`,
180
+ "package-lock.json"
181
+ )
182
+ );
183
+ }
184
+ }
185
+ return findings;
186
+ }
187
+ function checkDependencyConfusion(pkgName, npmrc, publicExists) {
188
+ const scoped = pkgName.startsWith("@");
189
+ const privateRegistry = /@[\w-]+\s*:\s*registry\s*=/.test(npmrc) || /registry\s*=\s*https?:\/\/(?!registry\.npmjs\.org)/.test(npmrc);
190
+ if (scoped && privateRegistry && publicExists) {
191
+ return [
192
+ finding(
193
+ "SUPPLY-CONFUSION-001",
194
+ "Private-scope name also exists on the public registry",
195
+ `${pkgName} is configured for a private registry but a public package exists`
196
+ )
197
+ ];
198
+ }
199
+ return [];
200
+ }
201
+ function checkProvenance(previouslyHad, currentlyHas) {
202
+ if (previouslyHad && !currentlyHas) {
203
+ return [
204
+ finding(
205
+ "SUPPLY-PROVENANCE-001",
206
+ "Package dropped npm provenance since the last scan",
207
+ "previous version had provenance; current version does not"
208
+ )
209
+ ];
210
+ }
211
+ return [];
212
+ }
213
+ async function installGuard(spec, opts = {}) {
214
+ if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY !== "1") {
215
+ return { findings: [], skipped: true };
216
+ }
217
+ const name = packageNameFromSpec(spec);
218
+ const url = `https://${REGISTRY_HOST}/${encodeURIComponent(name)}`;
219
+ assertRegistryUrl(url);
220
+ if (opts.fetchPackument) await opts.fetchPackument(url);
221
+ const findings = opts.extractDir !== void 0 ? analyzePackageDir(opts.extractDir) : [];
222
+ if (findings.length === 0 && opts.runNpm) {
223
+ await opts.runNpm(["install", spec, "--ignore-scripts"]);
224
+ }
225
+ return { findings };
226
+ }
227
+
228
+ export { analyzePackageDir, assertRegistryUrl, checkCooldown, checkDependencyConfusion, checkLockfileIntegrity, checkProvenance, diffPackageVersions, installGuard, packageNameFromSpec };
package/dist/threat.js ADDED
@@ -0,0 +1,63 @@
1
+ import { existsSync, statSync, readFileSync, readdirSync } from 'fs';
2
+ import { relative, join } from 'path';
3
+
4
+ // src/threat/index.ts
5
+ function walk(dir, out = []) {
6
+ if (!existsSync(dir)) return out;
7
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
8
+ const full = join(dir, entry.name);
9
+ if (entry.isDirectory()) {
10
+ if (["node_modules", "dist", ".git"].includes(entry.name)) continue;
11
+ walk(full, out);
12
+ } else if (entry.isFile() && /\.[cm]?[jt]sx?$/.test(entry.name)) {
13
+ out.push(full);
14
+ }
15
+ }
16
+ return out;
17
+ }
18
+ function buildThreatModel(dir, opts = {}) {
19
+ if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM !== "1") {
20
+ return { skipped: true, routes: [], stores: [], egress: [], stride: [], mermaid: "" };
21
+ }
22
+ const routes = [];
23
+ const stores = /* @__PURE__ */ new Set();
24
+ const egress = /* @__PURE__ */ new Set();
25
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
26
+ return { routes, stores: [], egress: [], stride: [], mermaid: "flowchart LR\n" };
27
+ }
28
+ for (const file of walk(dir)) {
29
+ const rel = relative(dir, file) || file;
30
+ const text = readFileSync(file, "utf8");
31
+ const routeRe = /app\.(get|post|put|patch|delete|use)\(\s*['"]([^'"]+)['"]/g;
32
+ let m;
33
+ while ((m = routeRe.exec(text)) !== null) {
34
+ routes.push({
35
+ method: (m[1] ?? "get").toUpperCase(),
36
+ path: m[2] ?? "/",
37
+ file: rel,
38
+ auth: /auth|requireUser|requireAuth/.test(text)
39
+ });
40
+ }
41
+ if (/sqlite|mongodb|postgres|mysql|createPool|DatabaseSync/.test(text)) stores.add(rel);
42
+ const urlRe = /https?:\/\/[^\s'"[`]+/g;
43
+ while ((m = urlRe.exec(text)) !== null) egress.add(m[0] ?? "");
44
+ }
45
+ const stride = routes.slice(0, 20).map((r) => ({
46
+ element: `${r.method} ${r.path}`,
47
+ spoofing: r.auth ? "auth middleware present" : "no auth seen",
48
+ tampering: "input reaches handler",
49
+ repudiation: "check access logs",
50
+ infoDisclosure: "review response bodies",
51
+ denialOfService: "no rate-limit inferred",
52
+ elevation: r.auth ? "review authorization" : "unauthenticated route"
53
+ }));
54
+ const mermaid = [
55
+ "flowchart LR",
56
+ " client[Client] --> app[App]",
57
+ ...routes.slice(0, 12).map((r, i) => ` app --> r${i}["${r.method} ${r.path}"]`),
58
+ ...[...stores].slice(0, 6).map((s, i) => ` app --> s${i}["store ${s}"]`)
59
+ ].join("\n");
60
+ return { routes, stores: [...stores], egress: [...egress], stride, mermaid };
61
+ }
62
+
63
+ export { buildThreatModel };
@@ -0,0 +1,23 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ // src/workspaces/index.ts
5
+ function listWorkspaces(dir, opts = {}) {
6
+ if (!opts.experimental && process.env.FULLSHIELD_EXPERIMENTAL_PLATFORM !== "1") return [];
7
+ const manifestPath = join(dir, "package.json");
8
+ if (!existsSync(manifestPath)) return [];
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(manifestPath, "utf8"));
11
+ if (Array.isArray(pkg.workspaces)) return pkg.workspaces;
12
+ if (Array.isArray(pkg.workspaces?.packages)) return pkg.workspaces.packages;
13
+ } catch {
14
+ return [];
15
+ }
16
+ const pnpm = join(dir, "pnpm-workspace.yaml");
17
+ if (existsSync(pnpm)) {
18
+ return readFileSync(pnpm, "utf8").split("\n").map((l) => l.replace(/^-+\s*/, "").trim()).filter((l) => l.length > 0 && !l.startsWith("packages:"));
19
+ }
20
+ return [];
21
+ }
22
+
23
+ export { listWorkspaces };
package/package.json CHANGED
@@ -1,13 +1,90 @@
1
1
  {
2
2
  "name": "fullshield",
3
- "version": "0.0.1",
4
- "description": "Full-cycle security for Node.js - in development",
5
- "main": "index.js",
3
+ "version": "0.1.0",
4
+ "description": "Full-cycle security scanner for Node.js/TypeScript backends: detect, classify, report, notify, fix, prevent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kathanp/fullshield.git"
10
+ },
11
+ "engines": {
12
+ "node": ">=22.13"
13
+ },
14
+ "bin": {
15
+ "fullshield": "dist/cli/index.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./runtime": "./dist/runtime/index.js",
25
+ "./runtime/express": "./dist/runtime/express.js",
26
+ "./runtime/fastify": "./dist/runtime/fastify.js",
27
+ "./runtime/rasp": "./dist/runtime/rasp.js",
28
+ "./runtime/register": "./dist/runtime/register.js",
29
+ "./runtime/auto": "./dist/runtime/auto.js",
30
+ "./cli": "./dist/cli/index.js",
31
+ "./package.json": "./package.json"
32
+ },
33
+ "peerDependencies": {
34
+ "express": "*",
35
+ "fastify": "*"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "express": {
39
+ "optional": true
40
+ },
41
+ "fastify": {
42
+ "optional": true
43
+ }
44
+ },
45
+ "files": [
46
+ "dist",
47
+ "!dist/**/*.map",
48
+ "README.md",
49
+ "LICENSE",
50
+ "CHANGELOG.md",
51
+ "templates",
52
+ "rules"
53
+ ],
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
6
57
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
58
+ "build": "tsup",
59
+ "typecheck": "tsc --noEmit",
60
+ "lint": "eslint . --max-warnings 0",
61
+ "test": "vitest run",
62
+ "test:watch": "vitest",
63
+ "test:coverage": "vitest run --coverage",
64
+ "test:e2e": "vitest run test/e2e",
65
+ "test:live-ai": "npm run build && node scripts/live-ai.mjs",
66
+ "bench": "node scripts/bench.mjs",
67
+ "verify": "node scripts/verify.mjs",
68
+ "verify:phase": "node scripts/verify.mjs --phase"
69
+ },
70
+ "dependencies": {
71
+ "@modelcontextprotocol/sdk": "^1.32.1",
72
+ "@typescript-eslint/typescript-estree": "^8.71.2",
73
+ "commander": "^12.1.0",
74
+ "ignore": "^5.3.2",
75
+ "picocolors": "^1.1.1",
76
+ "zod": "^4.6.7"
8
77
  },
9
- "keywords": [],
10
- "author": "",
11
- "license": "ISC",
12
- "type": "commonjs"
78
+ "devDependencies": {
79
+ "@eslint/js": "^9.14.0",
80
+ "@types/node": "^22.9.0",
81
+ "@vitest/coverage-v8": "^2.1.4",
82
+ "eslint": "^9.14.0",
83
+ "express": "^4.22.3",
84
+ "globals": "^15.12.0",
85
+ "tsup": "^8.3.5",
86
+ "typescript": "^5.6.3",
87
+ "typescript-eslint": "^8.13.0",
88
+ "vitest": "^2.1.4"
89
+ }
13
90
  }
@@ -0,0 +1,9 @@
1
+ # CONFIG-BODYLIMIT-001
2
+
3
+ **express.json() without a size limit.** CWE-770.
4
+
5
+ ## Remediation
6
+
7
+ Pass { limit: '100kb' } (or your budget).
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-CORS-001
2
+
3
+ **cors({ origin:'*', credentials:true }).** CWE-942.
4
+
5
+ ## Remediation
6
+
7
+ Use an explicit origin allow-list.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-DOCKER-ENV-001
2
+
3
+ **ENV *_SECRET= or *_KEY= in the Dockerfile.** CWE-798.
4
+
5
+ ## Remediation
6
+
7
+ Pass secrets at runtime, not build time.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-DOCKER-LATEST-001
2
+
3
+ **FROM image:latest.** CWE-1188.
4
+
5
+ ## Remediation
6
+
7
+ Pin a digest or a specific tag.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-DOCKER-ROOT-001
2
+
3
+ **Dockerfile has no USER instruction.** CWE-250.
4
+
5
+ ## Remediation
6
+
7
+ Add USER node (or another non-root user).
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-ERRSTACK-001
2
+
3
+ **err.stack sent in a response.** CWE-209.
4
+
5
+ ## Remediation
6
+
7
+ Log server-side; send a generic body to the client.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-HELMET-001
2
+
3
+ **express() app never calls helmet().** CWE-693.
4
+
5
+ ## Remediation
6
+
7
+ app.use(helmet()). Router-only files are not flagged.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-INSTALLSCRIPT-001
2
+
3
+ **Lockfile hasInstallScript.** CWE-506.
4
+
5
+ ## Remediation
6
+
7
+ Info only. Review the install script before trusting it.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # CONFIG-RATELIMIT-001
2
+
3
+ **Login/signup route with no rate limiter.** CWE-307.
4
+
5
+ ## Remediation
6
+
7
+ Apply express-rate-limit (or similar) to auth routes.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # DEPS-DEPRECATED-001
2
+
3
+ **Lockfile marks the package deprecated.** CWE-1104.
4
+
5
+ ## Remediation
6
+
7
+ Replace with a maintained alternative. FullShield does not invent registry data.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # DEPS-NOLOCK-001
2
+
3
+ **package.json is present but no lockfile.** Info.
4
+
5
+ Transitive versions cannot be audited. Exact versions pinned in `package.json` (no `^`/`~` range) are still queried against OSV.
6
+
7
+ ## Remediation
8
+
9
+ Commit `package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`.
@@ -0,0 +1,9 @@
1
+ # DEPS-TYPOSQUAT-001
2
+
3
+ **Direct dependency whose name is 1 edit from a popular package and is not itself in the static top-packages list.** CWE-1357.
4
+
5
+ ## Remediation
6
+
7
+ Confirm the name is intentional. Transitive names, hyphen-only variants, and packages that appear in the larger top-packages list are not flagged.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # DEPS-VULN-001
2
+
3
+ **OSV-known vulnerable package.** CWE-1104.
4
+
5
+ ## Remediation
6
+
7
+ Bump to the minimum non-vulnerable version from OSV. Same-major bumps are safe fixes.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,60 @@
1
+ # FullShield rules
2
+
3
+ Each Core rule has a positive fixture in `test/fixtures/vulnerable-app` and a
4
+ negative counterpart in `test/fixtures/clean-app`.
5
+
6
+ ## Secrets
7
+
8
+ | ID | What |
9
+ |---|---|
10
+ | [SECRET-AWS-001](SECRET-AWS-001.md) | AWS access key id |
11
+ | [SECRET-STRIPE-001](SECRET-STRIPE-001.md) | Stripe secret key |
12
+ | [SECRET-GITHUB-001](SECRET-GITHUB-001.md) | GitHub token |
13
+ | [SECRET-SLACK-001](SECRET-SLACK-001.md) | Slack token |
14
+ | [SECRET-GOOGLE-001](SECRET-GOOGLE-001.md) | Google API key |
15
+ | [SECRET-AIKEY-001](SECRET-AIKEY-001.md) | OpenAI/Anthropic-style key |
16
+ | [SECRET-PRIVKEY-001](SECRET-PRIVKEY-001.md) | PEM private key |
17
+ | [SECRET-DBURL-001](SECRET-DBURL-001.md) | DB URL with inline password |
18
+ | [SECRET-JWT-001](SECRET-JWT-001.md) | Hardcoded JWT secret |
19
+ | [SECRET-ENTROPY-001](SECRET-ENTROPY-001.md) | High-entropy assignment (low confidence) |
20
+ | [SECRET-ENVFILE-001](SECRET-ENVFILE-001.md) | Committed `.env` file |
21
+
22
+ ## Dependencies
23
+
24
+ | ID | What |
25
+ |---|---|
26
+ | [DEPS-VULN-001](DEPS-VULN-001.md) | OSV-known vulnerable package |
27
+ | [DEPS-TYPOSQUAT-001](DEPS-TYPOSQUAT-001.md) | Direct dep 1 edit from a popular name |
28
+ | [DEPS-DEPRECATED-001](DEPS-DEPRECATED-001.md) | Lockfile `deprecated` field |
29
+ | [DEPS-NOLOCK-001](DEPS-NOLOCK-001.md) | package.json without a lockfile |
30
+
31
+ ## SAST
32
+
33
+ | ID | What |
34
+ |---|---|
35
+ | [SAST-SQLI-001](SAST-SQLI-001.md) | Tainted SQL concatenation |
36
+ | [SAST-NOSQLI-001](SAST-NOSQLI-001.md) | Raw `req.body`/`query` as Mongo filter |
37
+ | [SAST-CMDI-001](SAST-CMDI-001.md) | Tainted shell `exec` |
38
+ | [SAST-PATH-001](SAST-PATH-001.md) | Tainted path without resolve+prefix |
39
+ | [SAST-EVAL-001](SAST-EVAL-001.md) | `eval` / `new Function` / `vm` / `*.eval` |
40
+ | [SAST-SSRF-001](SAST-SSRF-001.md) | Tainted URL to HTTP client |
41
+ | [SAST-REDIRECT-001](SAST-REDIRECT-001.md) | Tainted `res.redirect` |
42
+ | [SAST-JWT-001](SAST-JWT-001.md) | `jwt.decode` / `algorithms: none` |
43
+ | [SAST-CRYPTO-001](SAST-CRYPTO-001.md) | `Math.random` tokens / md5 passwords |
44
+ | [SAST-PROTO-001](SAST-PROTO-001.md) | Unguarded recursive merge |
45
+ | [SAST-XSS-001](SAST-XSS-001.md) | HTML `res.send` or `<%-` / `{{{` |
46
+ | [SAST-AUTH-001](SAST-AUTH-001.md) | Data route with no auth middleware |
47
+
48
+ ## Config
49
+
50
+ | ID | What |
51
+ |---|---|
52
+ | [CONFIG-HELMET-001](CONFIG-HELMET-001.md) | `express()` without `helmet()` |
53
+ | [CONFIG-BODYLIMIT-001](CONFIG-BODYLIMIT-001.md) | `express.json()` without `limit` |
54
+ | [CONFIG-CORS-001](CONFIG-CORS-001.md) | `origin:'*'` + `credentials:true` |
55
+ | [CONFIG-RATELIMIT-001](CONFIG-RATELIMIT-001.md) | Auth route without a limiter |
56
+ | [CONFIG-ERRSTACK-001](CONFIG-ERRSTACK-001.md) | `err.stack` sent to the client |
57
+ | [CONFIG-DOCKER-LATEST-001](CONFIG-DOCKER-LATEST-001.md) | `FROM *:latest` |
58
+ | [CONFIG-DOCKER-ROOT-001](CONFIG-DOCKER-ROOT-001.md) | Dockerfile with no `USER` |
59
+ | [CONFIG-DOCKER-ENV-001](CONFIG-DOCKER-ENV-001.md) | `ENV *_SECRET=` / `*_KEY=` |
60
+ | [CONFIG-INSTALLSCRIPT-001](CONFIG-INSTALLSCRIPT-001.md) | Lockfile `hasInstallScript` |
@@ -0,0 +1,9 @@
1
+ # SAST-AUTH-001
2
+
3
+ **Route handler that touches DB data with no auth-named middleware.** CWE-862.
4
+
5
+ ## Remediation
6
+
7
+ Low confidence. Add requireAuth (or similar) to the chain.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # SAST-CMDI-001
2
+
3
+ **Tainted input reaching exec/execSync/spawn({shell:true}).** CWE-78.
4
+
5
+ ## Remediation
6
+
7
+ Use execFile with an argument array and validate input.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # SAST-CRYPTO-001
2
+
3
+ **Math.random for tokens, or md5/sha1 near a password.** CWE-327.
4
+
5
+ ## Remediation
6
+
7
+ Use crypto.randomUUID / randomBytes and a modern KDF.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # SAST-EVAL-001
2
+
3
+ **eval, new Function, vm.runInNewContext, or lib.eval of a non-literal.** CWE-95.
4
+
5
+ ## Remediation
6
+
7
+ Do not evaluate user input. Use a parser or allow-list.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,9 @@
1
+ # SAST-JWT-001
2
+
3
+ **jwt.decode used as auth, algorithms includes none, or verify without an allow-list.** CWE-347.
4
+
5
+ ## Remediation
6
+
7
+ Call jwt.verify with algorithms: ['HS256'] (or your real list).
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.