fullshield 0.1.1 → 0.1.3

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 (52) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +43 -5
  3. package/dist/agent.js +3408 -607
  4. package/dist/bench/score.js +118 -0
  5. package/dist/cli/index.js +7137 -3385
  6. package/dist/compliance.js +31 -2
  7. package/dist/dashboard.js +20 -1
  8. package/dist/index.d.ts +152 -2
  9. package/dist/index.js +3773 -670
  10. package/dist/mcp.js +3430 -629
  11. package/dist/runtime/auto.js +100 -3
  12. package/dist/runtime/dlp.js +313 -0
  13. package/dist/runtime/express.js +337 -0
  14. package/dist/runtime/fastify.js +325 -0
  15. package/dist/runtime/hono.js +570 -0
  16. package/dist/runtime/koa.js +573 -0
  17. package/dist/runtime/rasp.js +100 -3
  18. package/dist/runtime/register.js +100 -3
  19. package/dist/supply.js +87 -1
  20. package/dist/vex.js +178 -0
  21. package/package.json +12 -1
  22. package/rules/CONFIG-DIRLIST-001.md +9 -0
  23. package/rules/CONFIG-TRUSTPROXY-001.md +11 -0
  24. package/rules/DLP-CARD-001.md +9 -0
  25. package/rules/DLP-INTERNAL-001.md +9 -0
  26. package/rules/DLP-PII-001.md +9 -0
  27. package/rules/DLP-SECRET-001.md +9 -0
  28. package/rules/DLP-STACK-001.md +9 -0
  29. package/rules/HOST-SSH-001.md +13 -0
  30. package/rules/HOST-SSH-002.md +11 -0
  31. package/rules/HOST-SSH-003.md +11 -0
  32. package/rules/MCP-001.md +7 -0
  33. package/rules/MCP-002.md +7 -0
  34. package/rules/MCP-003.md +7 -0
  35. package/rules/MCP-004.md +7 -0
  36. package/rules/MCP-005.md +7 -0
  37. package/rules/README.md +47 -1
  38. package/rules/SAST-COOKIE-001.md +11 -0
  39. package/rules/SAST-CSRF-001.md +11 -0
  40. package/rules/SAST-DESER-001.md +11 -0
  41. package/rules/SAST-GQL-001.md +11 -0
  42. package/rules/SAST-HASH-001.md +11 -0
  43. package/rules/SAST-MASSASSIGN-001.md +11 -0
  44. package/rules/SAST-NOSQLI-001.md +6 -2
  45. package/rules/SAST-REDOS-001.md +11 -0
  46. package/rules/SAST-TIMING-001.md +11 -0
  47. package/rules/SAST-UPLOAD-001.md +11 -0
  48. package/rules/SAST-XXE-001.md +11 -0
  49. package/rules/SECRET-BUNDLE-001.md +11 -0
  50. package/rules/SECRET-SESSION-001.md +9 -0
  51. package/rules/SUPPLY-WORKFLOW-001.md +9 -0
  52. package/rules/SUPPLY-WORM-001.md +9 -0
@@ -452,9 +452,25 @@ function sinkTexts(sink) {
452
452
  if (typeof sink === "number" || typeof sink === "boolean") return [String(sink)];
453
453
  return [];
454
454
  }
455
+ function whereJsChangesStructure(sink, ctx) {
456
+ let code = "";
457
+ if (typeof sink === "string") code = sink;
458
+ else if (sink !== null && typeof sink === "object" && !Array.isArray(sink) && "$where" in sink) {
459
+ const where = sink.$where;
460
+ if (typeof where === "string") code = where;
461
+ }
462
+ if (!code) return false;
463
+ for (const input of ctx.strings) {
464
+ if (input && sqlInputChangesStructure(code, input)) return true;
465
+ }
466
+ return false;
467
+ }
455
468
  function detectStructureChange(kind, sink, ctx, base) {
456
469
  if (kind === "proto") return protoPolluted(ctx);
457
- if (kind === "nosql") return nosqlUserOperators(sink, ctx);
470
+ if (kind === "nosql") {
471
+ if (nosqlUserOperators(sink, ctx)) return true;
472
+ return whereJsChangesStructure(sink, ctx);
473
+ }
458
474
  for (const text of sinkTexts(sink)) {
459
475
  for (const input of ctx.strings) {
460
476
  if (!input) continue;
@@ -756,11 +772,38 @@ function patchMethod(obj, key, wrapper) {
756
772
  record[key] = orig;
757
773
  });
758
774
  }
775
+ function sqlTextFrom(sql) {
776
+ if (typeof sql === "string") return sql;
777
+ if (Array.isArray(sql)) {
778
+ return sql.filter((part) => typeof part === "string").join(" ");
779
+ }
780
+ if (sql !== null && typeof sql === "object") {
781
+ const rec = sql;
782
+ if (typeof rec.text === "string") return rec.text;
783
+ if (typeof rec.sql === "string") return rec.sql;
784
+ if (Array.isArray(rec.strings)) {
785
+ return rec.strings.filter((part) => typeof part === "string").join(" ");
786
+ }
787
+ }
788
+ return "";
789
+ }
759
790
  function wrapSqlMethod(obj, key) {
760
791
  patchMethod(obj, key, (orig) => {
761
792
  return function(sql, ...rest) {
762
- const text = typeof sql === "string" ? sql : sql && typeof sql === "object" && "text" in sql && typeof sql.text === "string" ? sql.text : "";
763
- return applySink("sql", text, void 0, () => orig.call(this, sql, ...rest));
793
+ return applySink(
794
+ "sql",
795
+ sqlTextFrom(sql),
796
+ void 0,
797
+ () => orig.call(this, sql, ...rest)
798
+ );
799
+ };
800
+ });
801
+ }
802
+ function wrapNosqlWhere(obj, key) {
803
+ patchMethod(obj, key, (orig) => {
804
+ return function(expr, ...rest) {
805
+ const sink = typeof expr === "string" ? { $where: expr } : expr ?? {};
806
+ return applySink("nosql", sink, void 0, () => orig.call(this, expr, ...rest));
764
807
  };
765
808
  });
766
809
  }
@@ -1051,6 +1094,57 @@ function wrapKnownPackage(request, exported) {
1051
1094
  wrappedPackages.add(obj);
1052
1095
  return exported;
1053
1096
  }
1097
+ if (name === "@prisma/client" || name === "prisma" || name.endsWith("/@prisma/client")) {
1098
+ const client = exported.PrismaClient?.prototype;
1099
+ if (client) {
1100
+ wrapSqlMethod(client, "$queryRawUnsafe");
1101
+ wrapSqlMethod(client, "$executeRawUnsafe");
1102
+ wrapSqlMethod(client, "$queryRaw");
1103
+ wrapSqlMethod(client, "$executeRaw");
1104
+ }
1105
+ const Prisma = exported.Prisma;
1106
+ if (Prisma) wrapSqlMethod(Prisma, "raw");
1107
+ wrappedPackages.add(obj);
1108
+ return exported;
1109
+ }
1110
+ if (name === "sequelize" || name.endsWith("/sequelize")) {
1111
+ const rec = exported;
1112
+ const proto = rec.Sequelize?.prototype ?? rec.prototype;
1113
+ if (proto) wrapSqlMethod(proto, "query");
1114
+ if (typeof rec.literal === "function") wrapSqlMethod(rec, "literal");
1115
+ if (rec.Sequelize && typeof rec.Sequelize.literal === "function") wrapSqlMethod(rec.Sequelize, "literal");
1116
+ wrappedPackages.add(obj);
1117
+ return exported;
1118
+ }
1119
+ if (name === "knex" || name.endsWith("/knex")) {
1120
+ if (typeof exported.raw === "function") wrapSqlMethod(obj, "raw");
1121
+ const client = exported.Client?.prototype;
1122
+ if (client) {
1123
+ wrapSqlMethod(client, "raw");
1124
+ wrapSqlMethod(client, "whereRaw");
1125
+ wrapSqlMethod(client, "orderByRaw");
1126
+ wrapSqlMethod(client, "query");
1127
+ }
1128
+ wrappedPackages.add(obj);
1129
+ return exported;
1130
+ }
1131
+ if (name === "typeorm" || name.endsWith("/typeorm")) {
1132
+ const rec = exported;
1133
+ for (const cls of ["DataSource", "EntityManager", "QueryRunner", "Connection"]) {
1134
+ const proto = rec[cls]?.prototype;
1135
+ if (proto) wrapSqlMethod(proto, "query");
1136
+ }
1137
+ const qb = rec.SelectQueryBuilder?.prototype ?? rec.QueryBuilder?.prototype;
1138
+ if (qb) wrapSqlMethod(qb, "where");
1139
+ wrappedPackages.add(obj);
1140
+ return exported;
1141
+ }
1142
+ if (name === "drizzle-orm" || name.endsWith("/drizzle-orm")) {
1143
+ const sql = exported.sql;
1144
+ if (sql) wrapSqlMethod(sql, "raw");
1145
+ wrappedPackages.add(obj);
1146
+ return exported;
1147
+ }
1054
1148
  if (name === "mongodb" || name.includes("mongodb")) {
1055
1149
  const col = exported.Collection?.prototype;
1056
1150
  if (col) {
@@ -1071,7 +1165,10 @@ function wrapKnownPackage(request, exported) {
1071
1165
  return applySink("nosql", query ?? {}, void 0, () => orig.call(this, query, ...rest));
1072
1166
  };
1073
1167
  });
1168
+ wrapNosqlWhere(q, "$where");
1074
1169
  }
1170
+ const model = exported.Model?.prototype;
1171
+ if (model) wrapNosqlWhere(model, "$where");
1075
1172
  wrappedPackages.add(obj);
1076
1173
  return exported;
1077
1174
  }
package/dist/supply.js CHANGED
@@ -8,6 +8,11 @@ function makeFindingId(ruleId, file, snippet) {
8
8
  return createHash("sha256").update(`${ruleId}\0${file ?? ""}\0${normalized}`).digest("hex").slice(0, 16);
9
9
  }
10
10
 
11
+ // src/supply/experimental.ts
12
+ function experimentalVex() {
13
+ return process.env.FULLSHIELD_EXPERIMENTAL_VEX === "1";
14
+ }
15
+
11
16
  // src/supply/index.ts
12
17
  var REGISTRY_HOST = "registry.npmjs.org";
13
18
  var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
@@ -114,6 +119,10 @@ function analyzePackageDir(dir) {
114
119
  );
115
120
  }
116
121
  }
122
+ if (experimentalVex()) {
123
+ findings.push(...analyzeWormIndicators(dir));
124
+ findings.push(...analyzeWorkflowTampering(dir));
125
+ }
117
126
  return findings;
118
127
  }
119
128
  function diffPackageVersions(oldDir, newDir) {
@@ -224,5 +233,82 @@ async function installGuard(spec, opts = {}) {
224
233
  }
225
234
  return { findings };
226
235
  }
236
+ var TOKEN_RE = /(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/;
237
+ var PUBLISH_RE = /\bnpm\s+publish\b|\bnpmPublish\b|\blibnpmpublish\b/;
238
+ var REGISTRY_PUT_RE = /registry\.npmjs\.org[\s\S]{0,800}\bPUT\b|\bmethod\s*:\s*['"]PUT['"][\s\S]{0,400}registry\.npmjs\.org/;
239
+ var GITHUB_CREATE_RE = /api\.github\.com\/(?:user\/repos|repos\/)|\/actions\/workflows/;
240
+ var WORKFLOW_PATH_RE = /\.github\/workflows/;
241
+ var CI_WRITE_RE = /\.github\/|\.git\/hooks\/|\.gitlab-ci\.yml|\.travis\.yml|azure-pipelines(?:\.yml)?|Jenkinsfile|\.circleci\//;
242
+ function packageSources(dir) {
243
+ const manifestPath = join(dir, "package.json");
244
+ let scripts = "";
245
+ if (existsSync(manifestPath) && statSync(manifestPath).isFile()) {
246
+ try {
247
+ const pkg = readJson(manifestPath);
248
+ const block = pkg.scripts ?? {};
249
+ scripts = Object.values(block).join("\n");
250
+ } catch {
251
+ scripts = "";
252
+ }
253
+ }
254
+ const sources = walkJs(dir).map((file) => ({
255
+ file: relative(dir, file) || file,
256
+ text: readFileSync(file, "utf8")
257
+ }));
258
+ return { sources, scripts };
259
+ }
260
+ function analyzeWormIndicators(dir) {
261
+ const { sources, scripts } = packageSources(dir);
262
+ if (sources.length === 0 && scripts.length === 0) return [];
263
+ const text = `${sources.map((s) => s.text).join("\n")}
264
+ ${scripts}`;
265
+ if (!TOKEN_RE.test(text)) return [];
266
+ const replicates = PUBLISH_RE.test(text) || REGISTRY_PUT_RE.test(text) || GITHUB_CREATE_RE.test(text) || WORKFLOW_PATH_RE.test(text);
267
+ if (!replicates) return [];
268
+ const hit = sources.find(
269
+ (s) => TOKEN_RE.test(s.text) && (PUBLISH_RE.test(s.text) || REGISTRY_PUT_RE.test(s.text) || GITHUB_CREATE_RE.test(s.text) || WORKFLOW_PATH_RE.test(s.text))
270
+ ) ?? sources.find((s) => TOKEN_RE.test(s.text));
271
+ return [
272
+ finding(
273
+ "SUPPLY-WORM-001",
274
+ "Self-replicating worm indicators in dependency code",
275
+ "static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",
276
+ hit?.file,
277
+ "critical"
278
+ )
279
+ ];
280
+ }
281
+ function analyzeWorkflowTampering(dir) {
282
+ const { sources, scripts } = packageSources(dir);
283
+ const text = `${sources.map((s) => s.text).join("\n")}
284
+ ${scripts}`;
285
+ if (!CI_WRITE_RE.test(text)) return [];
286
+ const hit = sources.find((s) => CI_WRITE_RE.test(s.text));
287
+ return [
288
+ finding(
289
+ "SUPPLY-WORKFLOW-001",
290
+ "Dependency writes GitHub workflow, git hooks, or CI config",
291
+ "static: install script or package code writes .github/, .git/hooks/, or CI configuration",
292
+ hit?.file ?? (CI_WRITE_RE.test(scripts) ? "package.json" : void 0),
293
+ "critical"
294
+ )
295
+ ];
296
+ }
297
+ function provenanceCoverage(packages, known) {
298
+ let withProvenance = 0;
299
+ let unknown = 0;
300
+ for (const pkg of packages) {
301
+ const hit = known.get(`${pkg.name}@${pkg.version}`);
302
+ if (hit === true) withProvenance += 1;
303
+ else if (hit === void 0) unknown += 1;
304
+ }
305
+ const total = packages.length;
306
+ return {
307
+ total,
308
+ withProvenance,
309
+ unknown,
310
+ percent: total === 0 ? 0 : withProvenance / total * 100
311
+ };
312
+ }
227
313
 
228
- export { analyzePackageDir, assertRegistryUrl, checkCooldown, checkDependencyConfusion, checkLockfileIntegrity, checkProvenance, diffPackageVersions, installGuard, packageNameFromSpec };
314
+ export { analyzePackageDir, analyzeWorkflowTampering, analyzeWormIndicators, assertRegistryUrl, checkCooldown, checkDependencyConfusion, checkLockfileIntegrity, checkProvenance, diffPackageVersions, installGuard, packageNameFromSpec, provenanceCoverage };
package/dist/vex.js ADDED
@@ -0,0 +1,178 @@
1
+ import { createHash } from 'crypto';
2
+
3
+ // src/vex/index.ts
4
+
5
+ // src/sbom/cyclonedx.ts
6
+ function validateCycloneDx15(doc) {
7
+ const errors = [];
8
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
9
+ return ["CycloneDX document must be an object"];
10
+ }
11
+ const root = doc;
12
+ if (root.bomFormat !== "CycloneDX") errors.push('bomFormat must be "CycloneDX"');
13
+ if (root.specVersion !== "1.5") errors.push('specVersion must be "1.5"');
14
+ if (!Array.isArray(root.components)) errors.push("components must be an array");
15
+ return errors;
16
+ }
17
+
18
+ // src/vex/index.ts
19
+ var OPENVEX_CONTEXT = "https://openvex.dev/ns/v0.2.0";
20
+ var OPENVEX_NOT_AFFECTED_JUSTIFICATION = "vulnerable_code_not_in_execute_path";
21
+ var CDX_NOT_AFFECTED_JUSTIFICATION = "code_not_reachable";
22
+ var OPENVEX_STATUSES = /* @__PURE__ */ new Set(["not_affected", "affected", "fixed", "under_investigation"]);
23
+ function depFindings(findings) {
24
+ return findings.filter((f) => f.ruleId === "DEPS-VULN-001");
25
+ }
26
+ function vulnName(finding) {
27
+ return finding.references?.find((r) => r.startsWith("CVE-") || r.startsWith("GHSA-") || r.startsWith("MAL-")) ?? finding.id;
28
+ }
29
+ function npmPurl(snippet) {
30
+ const raw = snippet ?? "unknown@0.0.0";
31
+ let name;
32
+ let version;
33
+ if (raw.startsWith("@")) {
34
+ const second = raw.indexOf("@", 1);
35
+ name = second === -1 ? raw : raw.slice(0, second);
36
+ version = second === -1 ? "0.0.0" : raw.slice(second + 1);
37
+ } else {
38
+ const at = raw.indexOf("@");
39
+ name = at <= 0 ? raw : raw.slice(0, at);
40
+ version = at <= 0 ? "0.0.0" : raw.slice(at + 1);
41
+ }
42
+ return `pkg:npm/${encodeURIComponent(name)}@${version}`;
43
+ }
44
+ function notImported(finding) {
45
+ return finding.reachability === "not-imported";
46
+ }
47
+ function openVexStatement(finding) {
48
+ const base = {
49
+ vulnerability: { name: vulnName(finding) },
50
+ products: [{ "@id": npmPurl(finding.snippet) }]
51
+ };
52
+ if (notImported(finding)) {
53
+ return {
54
+ ...base,
55
+ status: "not_affected",
56
+ justification: OPENVEX_NOT_AFFECTED_JUSTIFICATION
57
+ };
58
+ }
59
+ return { ...base, status: "under_investigation" };
60
+ }
61
+ function buildOpenVex(findings) {
62
+ const statements = depFindings(findings).map(openVexStatement);
63
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
64
+ const idSeed = JSON.stringify(statements);
65
+ const digest = createHash("sha256").update(idSeed).digest("hex").slice(0, 16);
66
+ return {
67
+ "@context": OPENVEX_CONTEXT,
68
+ "@id": `https://openvex.dev/docs/public/fullshield-${digest}`,
69
+ author: "FullShield",
70
+ timestamp,
71
+ version: 1,
72
+ statements
73
+ };
74
+ }
75
+ function validateOpenVex(doc) {
76
+ const errors = [];
77
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
78
+ return ["OpenVEX document must be an object"];
79
+ }
80
+ const root = doc;
81
+ if (typeof root["@context"] !== "string" || !String(root["@context"]).includes("openvex")) {
82
+ errors.push("@context must be an OpenVEX context URI");
83
+ }
84
+ if (typeof root["@id"] !== "string" || root["@id"].length === 0) errors.push("@id is required");
85
+ if (typeof root.author !== "string" || root.author.length === 0) errors.push("author is required");
86
+ if (typeof root.timestamp !== "string" || root.timestamp.length === 0) errors.push("timestamp is required");
87
+ if (typeof root.version !== "number") errors.push("version must be a number");
88
+ if (!Array.isArray(root.statements)) {
89
+ errors.push("statements must be an array");
90
+ return errors;
91
+ }
92
+ for (const [i, raw] of root.statements.entries()) {
93
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
94
+ errors.push(`statements[${i}] must be an object`);
95
+ continue;
96
+ }
97
+ const stmt = raw;
98
+ if (typeof stmt.vulnerability !== "object" || stmt.vulnerability === null) {
99
+ errors.push(`statements[${i}].vulnerability is required`);
100
+ }
101
+ if (!Array.isArray(stmt.products) || stmt.products.length === 0) {
102
+ errors.push(`statements[${i}].products must be a non-empty array`);
103
+ }
104
+ if (typeof stmt.status !== "string" || !OPENVEX_STATUSES.has(stmt.status)) {
105
+ errors.push(`statements[${i}].status is invalid`);
106
+ continue;
107
+ }
108
+ if (stmt.status === "not_affected") {
109
+ if (stmt.justification !== OPENVEX_NOT_AFFECTED_JUSTIFICATION) {
110
+ errors.push(
111
+ `statements[${i}]: not_affected requires justification ${OPENVEX_NOT_AFFECTED_JUSTIFICATION}`
112
+ );
113
+ }
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+ function cdxAnalysis(finding) {
119
+ if (notImported(finding)) {
120
+ return { state: "not_affected", justification: CDX_NOT_AFFECTED_JUSTIFICATION };
121
+ }
122
+ return { state: "in_triage" };
123
+ }
124
+ function buildCycloneDxVex(meta, findings) {
125
+ return {
126
+ $schema: "http://cyclonedx.org/schema/bom-1.5.schema.json",
127
+ bomFormat: "CycloneDX",
128
+ specVersion: "1.5",
129
+ version: 1,
130
+ metadata: {
131
+ component: { type: "application", name: meta.name, version: meta.version }
132
+ },
133
+ components: [],
134
+ vulnerabilities: depFindings(findings).map((f) => ({
135
+ id: vulnName(f),
136
+ description: f.message,
137
+ affects: [{ ref: npmPurl(f.snippet) }],
138
+ analysis: cdxAnalysis(f)
139
+ }))
140
+ };
141
+ }
142
+ function validateCycloneDxVex(doc) {
143
+ const errors = validateCycloneDx15(doc);
144
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return errors;
145
+ const root = doc;
146
+ if (!Array.isArray(root.vulnerabilities)) {
147
+ errors.push("vulnerabilities must be an array");
148
+ return errors;
149
+ }
150
+ for (const [i, raw] of root.vulnerabilities.entries()) {
151
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
152
+ errors.push(`vulnerabilities[${i}] must be an object`);
153
+ continue;
154
+ }
155
+ const vuln = raw;
156
+ if (typeof vuln.analysis !== "object" || vuln.analysis === null || Array.isArray(vuln.analysis)) {
157
+ errors.push(`vulnerabilities[${i}].analysis is required`);
158
+ continue;
159
+ }
160
+ const analysis = vuln.analysis;
161
+ if (typeof analysis.state !== "string" || analysis.state.length === 0) {
162
+ errors.push(`vulnerabilities[${i}].analysis.state is required`);
163
+ continue;
164
+ }
165
+ if (analysis.state === "not_affected" && analysis.justification !== CDX_NOT_AFFECTED_JUSTIFICATION) {
166
+ errors.push(
167
+ `vulnerabilities[${i}]: not_affected requires justification ${CDX_NOT_AFFECTED_JUSTIFICATION}`
168
+ );
169
+ }
170
+ }
171
+ return errors;
172
+ }
173
+ function buildVex(findings, format, meta = { name: "app", version: "0.0.0" }) {
174
+ if (format === "cyclonedx") return buildCycloneDxVex(meta, findings);
175
+ return buildOpenVex(findings);
176
+ }
177
+
178
+ export { buildCycloneDxVex, buildOpenVex, buildVex, validateCycloneDxVex, validateOpenVex };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullshield",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Full-cycle security scanner for Node.js/TypeScript backends: detect, classify, report, notify, fix, prevent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,6 +24,8 @@
24
24
  "./runtime": "./dist/runtime/index.js",
25
25
  "./runtime/express": "./dist/runtime/express.js",
26
26
  "./runtime/fastify": "./dist/runtime/fastify.js",
27
+ "./runtime/koa": "./dist/runtime/koa.js",
28
+ "./runtime/hono": "./dist/runtime/hono.js",
27
29
  "./runtime/rasp": "./dist/runtime/rasp.js",
28
30
  "./runtime/register": "./dist/runtime/register.js",
29
31
  "./runtime/auto": "./dist/runtime/auto.js",
@@ -33,6 +35,8 @@
33
35
  "peerDependencies": {
34
36
  "express": "*",
35
37
  "fastify": "*",
38
+ "koa": "*",
39
+ "hono": "*",
36
40
  "@modelcontextprotocol/sdk": "^1.32.1",
37
41
  "zod": "^4.6.7"
38
42
  },
@@ -43,6 +47,12 @@
43
47
  "fastify": {
44
48
  "optional": true
45
49
  },
50
+ "koa": {
51
+ "optional": true
52
+ },
53
+ "hono": {
54
+ "optional": true
55
+ },
46
56
  "@modelcontextprotocol/sdk": {
47
57
  "optional": true
48
58
  },
@@ -72,6 +82,7 @@
72
82
  "test:e2e": "vitest run test/e2e",
73
83
  "test:live-ai": "npm run build && node scripts/live-ai.mjs",
74
84
  "bench": "node scripts/bench.mjs",
85
+ "bench:public": "node scripts/bench-public.mjs",
75
86
  "verify": "node scripts/verify.mjs",
76
87
  "verify:phase": "node scripts/verify.mjs --phase"
77
88
  },
@@ -0,0 +1,9 @@
1
+ # CONFIG-DIRLIST-001
2
+
3
+ **`serveIndex()` enables HTTP directory listing.** CWE-548.
4
+
5
+ ## Remediation
6
+
7
+ Remove `serve-index`, or put the route behind authentication. Do not list ftp, logs, or key directories.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §4 and the fixtures under `test/fixtures/`.
@@ -0,0 +1,11 @@
1
+ # CONFIG-TRUSTPROXY-001
2
+
3
+ **`trust proxy` set to `true` while IP-based rate limiting is used (spoofable `X-Forwarded-For`).** CWE-290.
4
+
5
+ Experimental (`FULLSHIELD_EXPERIMENTAL_CLASSES=1`).
6
+
7
+ ## Remediation
8
+
9
+ Set `trust proxy` to a hop count or subnet, not `true`. Key rate limits on a verified identity, not the raw client IP.
10
+
11
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 29.
@@ -0,0 +1,9 @@
1
+ # DLP-CARD-001
2
+
3
+ **Outgoing response contains a Luhn-valid card number.** CWE-359. Experimental (`FULLSHIELD_EXPERIMENTAL_DLP=1`).
4
+
5
+ ## Remediation
6
+
7
+ Never return full PANs. Store tokens; show only the last four digits if needed.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 32 and the fixtures under `test/fixtures/dlp/`.
@@ -0,0 +1,9 @@
1
+ # DLP-INTERNAL-001
2
+
3
+ **Outgoing response contains an internal IP or hostname.** CWE-200. Experimental (`FULLSHIELD_EXPERIMENTAL_DLP=1`).
4
+
5
+ ## Remediation
6
+
7
+ Do not expose RFC1918 / loopback / `*.internal` addresses in client responses.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 32 and the fixtures under `test/fixtures/dlp/`.
@@ -0,0 +1,9 @@
1
+ # DLP-PII-001
2
+
3
+ **Outgoing response contains bulk emails on a route that normally returns one.** CWE-359. Experimental (`FULLSHIELD_EXPERIMENTAL_DLP=1`).
4
+
5
+ ## Remediation
6
+
7
+ Scope list/export endpoints. Do not dump address books from a single-user route.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 32 and the fixtures under `test/fixtures/dlp/`.
@@ -0,0 +1,9 @@
1
+ # DLP-SECRET-001
2
+
3
+ **Outgoing response contains a secret pattern** (AWS, Stripe, GitHub, Slack, Google, AI, PEM, DB URL). CWE-200. Experimental (`FULLSHIELD_EXPERIMENTAL_DLP=1`).
4
+
5
+ ## Remediation
6
+
7
+ Do not send credentials in API responses. Rotate anything that leaked. Prefer `shieldGuard({ dlp: { mode: 'monitor' } })` until the benign corpus is measured.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 32 and the fixtures under `test/fixtures/dlp/`.
@@ -0,0 +1,9 @@
1
+ # DLP-STACK-001
2
+
3
+ **Outgoing response contains a stack trace.** CWE-209. Experimental (`FULLSHIELD_EXPERIMENTAL_DLP=1`).
4
+
5
+ ## Remediation
6
+
7
+ Log stacks server-side. Return a generic error body to clients.
8
+
9
+ See `SECURITY_SCANNER_SPEC.md` §24 Phase 32 and the fixtures under `test/fixtures/dlp/`.
@@ -0,0 +1,13 @@
1
+ # HOST-SSH-001
2
+
3
+ **OpenSSH package version is in the CVE-2024-6387 (regreSSHion) range.** CWE-364.
4
+
5
+ Uses the distro package version (`dpkg-query` / `rpm` / `apk`), not `ssh -V`. Distro OSV advisories win so backported fixes are not flagged. Upstream fallback: 8.5p1 ≤ v < 9.8p1, or v < 4.4p1.
6
+
7
+ Severity is **critical** only when the version is vulnerable, `LoginGraceTime` is not 0, and sshd is listening on a non-loopback address.
8
+
9
+ ## Remediation
10
+
11
+ Upgrade `openssh-server` via the distro package manager. Temporary mitigation: `LoginGraceTime 0` (enables a connection-exhaustion DoS). FullShield does not auto-patch or stop OS/SSH exploits.
12
+
13
+ Experimental: `fullshield host --experimental-host`.
@@ -0,0 +1,11 @@
1
+ # HOST-SSH-002
2
+
3
+ **Vulnerable OpenSSH without the `LoginGraceTime 0` mitigation.** CWE-693.
4
+
5
+ Emitted only when HOST-SSH-001 applies and effective sshd config (`sshd -T`, or `/etc/ssh/sshd_config` plus `Include` files if `sshd -T` is not permitted) does not have `logingracetime 0`.
6
+
7
+ ## Remediation
8
+
9
+ Set `LoginGraceTime 0` and reload sshd as a temporary mitigation. Note: this enables a connection-exhaustion DoS. Prefer upgrading `openssh-server`. FullShield does not change sshd configuration.
10
+
11
+ Experimental: `fullshield host --experimental-host`.
@@ -0,0 +1,11 @@
1
+ # HOST-SSH-003
2
+
3
+ **Possible SSH authentication-timeout exploitation attempts.** CWE-307.
4
+
5
+ Counts `Timeout before authentication` lines per source IP in the last 24h from `journalctl -u ssh*` or `/var/log/auth.log` when those files are readable **without sudo**. Never a confirmed attack.
6
+
7
+ ## Remediation
8
+
9
+ Use fail2ban or CrowdSec to block abusive sources. FullShield does not install firewall rules or block IPs.
10
+
11
+ Experimental: `fullshield host --experimental-host`.
@@ -0,0 +1,7 @@
1
+ # MCP-001
2
+
3
+ **Secret inlined in an MCP client config env block.** CWE-798. Experimental (`FULLSHIELD_EXPERIMENTAL_MCPCFG=1`). High.
4
+
5
+ ## Remediation
6
+
7
+ Replace the value with an environment-variable reference (`${GITHUB_TOKEN}`) and rotate the exposed credential. Removing it from the file is not enough: it remains in git history.
@@ -0,0 +1,7 @@
1
+ # MCP-002
2
+
3
+ **MCP server launched via unpinned `npx <pkg>` or `@latest`.** CWE-494. Experimental (`FULLSHIELD_EXPERIMENTAL_MCPCFG=1`). Medium.
4
+
5
+ ## Remediation
6
+
7
+ Pin the package to an exact version (`@scope/pkg@1.2.3`). Do not use `@latest` or a bare package name.
@@ -0,0 +1,7 @@
1
+ # MCP-003
2
+
3
+ **MCP server exposes shell/exec or unrestricted filesystem-write tools without an allowlist.** CWE-250. Experimental (`FULLSHIELD_EXPERIMENTAL_MCPCFG=1`). Medium.
4
+
5
+ ## Remediation
6
+
7
+ Restrict filesystem roots to project directories. Do not register shell/exec/write tools unless an allowlist (and a confirm step) is enforced. See `guardTools`.
@@ -0,0 +1,7 @@
1
+ # MCP-004
2
+
3
+ **MCP tool description contains instruction-injection text, hidden markup, or zero-width characters.** CWE-74. Experimental (`FULLSHIELD_EXPERIMENTAL_MCPCFG=1`). High.
4
+
5
+ ## Remediation
6
+
7
+ Keep descriptions literal and user-facing. Strip zero-width / bidi-override characters and any text aimed at the model ("ignore previous", hidden HTML comments).
@@ -0,0 +1,7 @@
1
+ # MCP-005
2
+
3
+ **Remote MCP server configured over plaintext `http://`.** CWE-319. Experimental (`FULLSHIELD_EXPERIMENTAL_MCPCFG=1`). Medium.
4
+
5
+ ## Remediation
6
+
7
+ Use `https://` so tokens and tool traffic are not sent in the clear.