fullshield 0.0.1 → 0.1.1
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.
- package/CHANGELOG.md +87 -0
- package/LICENSE +21 -0
- package/README.md +159 -1
- package/dist/agent.js +5759 -0
- package/dist/cli/index.js +8612 -0
- package/dist/compliance.js +126 -0
- package/dist/confirm.js +146 -0
- package/dist/dashboard.js +143 -0
- package/dist/iac.js +84 -0
- package/dist/incremental.js +13 -0
- package/dist/index.d.ts +334 -0
- package/dist/index.js +6199 -0
- package/dist/llm.js +105 -0
- package/dist/mcp.js +5869 -0
- package/dist/runtime/auto.js +1163 -0
- package/dist/runtime/express.js +242 -0
- package/dist/runtime/fastify.js +254 -0
- package/dist/runtime/index.js +229 -0
- package/dist/runtime/rasp/loader.js +14 -0
- package/dist/runtime/rasp.js +1339 -0
- package/dist/runtime/register.js +1171 -0
- package/dist/supply.js +228 -0
- package/dist/threat.js +63 -0
- package/dist/workspaces.js +23 -0
- package/package.json +93 -8
- package/rules/CONFIG-BODYLIMIT-001.md +9 -0
- package/rules/CONFIG-CORS-001.md +9 -0
- package/rules/CONFIG-DOCKER-ENV-001.md +9 -0
- package/rules/CONFIG-DOCKER-LATEST-001.md +9 -0
- package/rules/CONFIG-DOCKER-ROOT-001.md +9 -0
- package/rules/CONFIG-ERRSTACK-001.md +9 -0
- package/rules/CONFIG-HELMET-001.md +9 -0
- package/rules/CONFIG-INSTALLSCRIPT-001.md +9 -0
- package/rules/CONFIG-RATELIMIT-001.md +9 -0
- package/rules/DEPS-DEPRECATED-001.md +9 -0
- package/rules/DEPS-NOLOCK-001.md +9 -0
- package/rules/DEPS-TYPOSQUAT-001.md +9 -0
- package/rules/DEPS-VULN-001.md +9 -0
- package/rules/README.md +60 -0
- package/rules/SAST-AUTH-001.md +9 -0
- package/rules/SAST-CMDI-001.md +9 -0
- package/rules/SAST-CRYPTO-001.md +9 -0
- package/rules/SAST-EVAL-001.md +9 -0
- package/rules/SAST-JWT-001.md +9 -0
- package/rules/SAST-NOSQLI-001.md +9 -0
- package/rules/SAST-PATH-001.md +9 -0
- package/rules/SAST-PROTO-001.md +9 -0
- package/rules/SAST-REDIRECT-001.md +9 -0
- package/rules/SAST-SQLI-001.md +9 -0
- package/rules/SAST-SSRF-001.md +9 -0
- package/rules/SAST-XSS-001.md +9 -0
- package/rules/SECRET-AIKEY-001.md +9 -0
- package/rules/SECRET-AWS-001.md +9 -0
- package/rules/SECRET-DBURL-001.md +9 -0
- package/rules/SECRET-ENTROPY-001.md +9 -0
- package/rules/SECRET-ENVFILE-001.md +9 -0
- package/rules/SECRET-GITHUB-001.md +9 -0
- package/rules/SECRET-GOOGLE-001.md +9 -0
- package/rules/SECRET-JWT-001.md +9 -0
- package/rules/SECRET-PRIVKEY-001.md +9 -0
- package/rules/SECRET-SLACK-001.md +9 -0
- package/rules/SECRET-STRIPE-001.md +9 -0
- package/templates/fullshield.config.json +7 -0
- package/templates/github-watch.yml +15 -0
- package/templates/github-workflow.yml +19 -0
- package/templates/gitlab-ci.yml +11 -0
- 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,98 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fullshield",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Full-cycle security for Node.js
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
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
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
37
|
+
"zod": "^4.6.7"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"express": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"fastify": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@modelcontextprotocol/sdk": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"zod": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"!dist/**/*.map",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"CHANGELOG.md",
|
|
59
|
+
"templates",
|
|
60
|
+
"rules"
|
|
61
|
+
],
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
6
65
|
"scripts": {
|
|
7
|
-
"
|
|
66
|
+
"build": "tsup",
|
|
67
|
+
"typecheck": "tsc --noEmit",
|
|
68
|
+
"lint": "eslint . --max-warnings 0",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"test:coverage": "vitest run --coverage",
|
|
72
|
+
"test:e2e": "vitest run test/e2e",
|
|
73
|
+
"test:live-ai": "npm run build && node scripts/live-ai.mjs",
|
|
74
|
+
"bench": "node scripts/bench.mjs",
|
|
75
|
+
"verify": "node scripts/verify.mjs",
|
|
76
|
+
"verify:phase": "node scripts/verify.mjs --phase"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@typescript-eslint/typescript-estree": "^8.71.2",
|
|
80
|
+
"commander": "^12.1.0",
|
|
81
|
+
"ignore": "^5.3.2",
|
|
82
|
+
"picocolors": "^1.1.1"
|
|
8
83
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
86
|
+
"@eslint/js": "^9.14.0",
|
|
87
|
+
"@types/node": "^22.9.0",
|
|
88
|
+
"@vitest/coverage-v8": "^2.1.4",
|
|
89
|
+
"eslint": "^9.14.0",
|
|
90
|
+
"express": "^4.22.3",
|
|
91
|
+
"globals": "^15.12.0",
|
|
92
|
+
"tsup": "^8.3.5",
|
|
93
|
+
"typescript": "^5.6.3",
|
|
94
|
+
"typescript-eslint": "^8.13.0",
|
|
95
|
+
"vitest": "^2.1.4",
|
|
96
|
+
"zod": "^4.6.7"
|
|
97
|
+
}
|
|
13
98
|
}
|
|
@@ -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/`.
|
package/rules/README.md
ADDED
|
@@ -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-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/`.
|