fullshield 0.1.0 → 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 CHANGED
@@ -4,9 +4,19 @@ All notable changes to FullShield are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
- - Runtime requirement is Node.js **>= 22.13** (Node 20 is EOL). CI matrix is Node 22 and 24 on Linux, macOS, and Windows. `node:sqlite` is unflagged from 22.13.0 and is loaded only when history/confirm actually open a database — `node --import fullshield/runtime/register -e "1"` must not emit a SQLite `ExperimentalWarning`.
8
- - Dashboard `HistoryDb.close()` releases `history.db` (Windows cannot unlink an open SQLite handle).
9
- - Remediation agent runs `npm.cmd` on Windows and restores untracked files without `git checkout --`.
7
+ ## 0.1.1 — 2026-10-15
8
+
9
+ ### Phase 25 — 0.1.1 hardening (Windows + lazy SQLite)
10
+
11
+ - CI matrix is Linux, macOS, and Windows × Node 22 and 24.
12
+ - `node:sqlite` is loaded only when history/confirm open a database. `node --import fullshield/runtime/register -e "1"` prints nothing on stderr. Runtime entry points do not statically import sqlite.
13
+ - Dashboard `HistoryDb.close()` is idempotent and uses try/finally so Windows can unlink `history.db`.
14
+ - Fixer/agent diffs are `core.autocrlf`-independent: normalize to `\n` for compare, restore the file's original EOL on apply. Git pathspecs use POSIX slashes.
15
+ - `--import` child processes use `pathToFileURL(...).href` (`importHref`), never a bare absolute path.
16
+ - Flood test measures retained-heap delta after GC (< 10 MB) and asserts LRU sizes (512 / 64 / 128). Rate limiter covers the exact window edge and empty-then-refill.
17
+ - CI pins `ubuntu-24.04` and uses checkout/setup-node/upload-artifact v7 (Node 24 runtime).
18
+ - `@modelcontextprotocol/sdk` and `zod` are optional peerDependencies, lazy-imported only by `fullshield mcp`. Other scan/runtime deps stay required.
19
+ - `npm pack` ships `README.md` at the tarball root. `package.json` has no empty `readme` field (that left the npm packument as `"readme": ""`).
10
20
 
11
21
  ## 0.1.0 — 2026-10-14
12
22
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # FullShield
2
2
 
3
- **v0.1.0 / alpha.** One install for Node.js/TypeScript backends: detect → classify → report → notify → fix → gate (CI `--fail-on` + runtime monitor). It does not stop attacks by itself.
3
+ **v0.1.1 / alpha.** One install for Node.js/TypeScript backends: detect → classify → report → notify → fix → gate (CI `--fail-on` + runtime monitor). It does not stop attacks by itself.
4
4
 
5
5
  **Headline: dependency reachability.** A lockfile CVE is not the same as an unused package. FullShield classifies each OSV hit as `reachable` (imported and called), `imported-not-called` (−1), `reachable-via-dependency` (lockfile path from an imported direct dep — **severity kept**, path shown, e.g. `express > body-parser > qs`), `not-imported` (−2, only when no such path exists), or `reachability-unknown` (severity kept) when the lockfile format has no parsed dependency edges. DevDependency-only vulns are `info` with a `dev-only` note unless production sources import the package. Pair it with `fullshield sbom --format cyclonedx`.
6
6
 
@@ -60,6 +60,10 @@ This is a `0.x` release. APIs may change. Do not treat it as a penetration test.
60
60
 
61
61
  ## Known limitations
62
62
 
63
+ - Platforms: Linux, macOS, and Windows on Node 22.13+. Quoted filenames in dirty-tree tests are POSIX-only (Windows forbids `"` in names).
64
+ - `node:sqlite` is loaded only by history/confirm, not by `--import fullshield/runtime/register`. Dashboard closes every handle in `try/finally` (Windows cannot delete an open `history.db`).
65
+ - FullShield does not stop OS or SSH exploits (including regreSSHion). It scans Node/TS apps you own.
66
+
63
67
  Experimental RASP structure-change scoring on the vendored PayloadsAllTheThings corpus (not a claim that live attacks are blocked):
64
68
 
65
69
  - **SQL 42/42.** Quote-breakouts inside `'…'` (`' OR 1=1 --`, `'; DROP TABLE x; --`) are detected. Payloads `"-"`, `" "`, `"&"`, `"^"` were moved to the benign set: they stay inside one single-quoted string token and are not exploitable in that sink.
@@ -113,7 +117,7 @@ app.use(shieldGuard({ mode: 'monitor', authRoutes: ['/login'] }));
113
117
  | `edge` | GET/HEAD posture check on a URL you pass: CDN/WAF, HTTPS redirect, security headers, `/.env` / `/.git` exposure. No crawl, no payloads. |
114
118
  | `install` | **Experimental** install-guard (`FULLSHIELD_EXPERIMENTAL_SUPPLY=1`). Static analysis only; never runs package scripts. Registry reads are `registry.npmjs.org` only. |
115
119
  | `confirm` | **Experimental** exploit confirm (`FULLSHIELD_EXPERIMENTAL_CONFIRM=1`). localhost / 127.0.0.1 / FullShield-started container only. Harmless canaries only. |
116
- | `mcp` | **Experimental** MCP server (`FULLSHIELD_EXPERIMENTAL_MCP=1`). Official `@modelcontextprotocol/sdk` stdio: `fullshield mcp --stdio`. Read-only tools: `scan`, `get_findings`, `explain_finding`, `propose_fix` (diff only, never writes), `verify_fix` (temp copy). |
120
+ | `mcp` | **Experimental** MCP server (`FULLSHIELD_EXPERIMENTAL_MCP=1`). Official SDK stdio: `fullshield mcp --stdio`. Requires optional peers `@modelcontextprotocol/sdk` and `zod` (`npm i -D @modelcontextprotocol/sdk zod`); missing peers print an install hint, not a stack dump. Read-only tools: `scan`, `get_findings`, `explain_finding`, `propose_fix` (diff only, never writes), `verify_fix` (temp copy). |
117
121
  | `honey init` | **Experimental** honeytokens (`FULLSHIELD_EXPERIMENTAL_HONEY=1`). Decoy routes are **low** severity and aggregated per IP; only use of a canary **credential** is critical. Auto-block is off unless `threatAutoBlock` is set (shared-IP / NAT risk; max duration applies). Decoys are fake values added to `.gitignore`. |
118
122
  | `threat-model` | **Experimental** STRIDE + Mermaid (`FULLSHIELD_EXPERIMENTAL_PLATFORM=1`). |
119
123
  | `report --serve` | **Experimental** local dashboard (`FULLSHIELD_EXPERIMENTAL_PLATFORM=1`). Binds **127.0.0.1 only** and rejects any `Host` other than localhost/127.0.0.1 (DNS rebinding). No state-changing GET. |
package/dist/agent.js CHANGED
@@ -5489,11 +5489,25 @@ function scan(options = {}) {
5489
5489
  return runScan(options);
5490
5490
  }
5491
5491
 
5492
+ // src/fixer/eol.ts
5493
+ function detectEol(text) {
5494
+ return text.includes("\r\n") ? "\r\n" : "\n";
5495
+ }
5496
+ function normalizeNewlines(text) {
5497
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5498
+ }
5499
+ function applyEol(text, eol) {
5500
+ const normalized = normalizeNewlines(text);
5501
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
5502
+ }
5503
+
5492
5504
  // src/fixer/diff.ts
5493
5505
  function unifiedDiff(filename, before, after) {
5494
- if (before === after) return "";
5495
- const oldLines = before.split("\n");
5496
- const newLines = after.split("\n");
5506
+ const oldNorm = normalizeNewlines(before);
5507
+ const newNorm = normalizeNewlines(after);
5508
+ if (oldNorm === newNorm) return "";
5509
+ const oldLines = oldNorm.split("\n");
5510
+ const newLines = newNorm.split("\n");
5497
5511
  const hunks = [`--- a/${filename}`, `+++ b/${filename}`];
5498
5512
  hunks.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);
5499
5513
  for (const line of oldLines) hunks.push(`-${line}`);
@@ -5515,8 +5529,9 @@ function proposeFix(finding, source) {
5515
5529
  return void 0;
5516
5530
  }
5517
5531
  function helmetFix(file, source) {
5518
- if (/helmet\s*\(/.test(source)) return void 0;
5519
- let after = source;
5532
+ const normalized = normalizeNewlines(source);
5533
+ if (/helmet\s*\(/.test(normalized)) return void 0;
5534
+ let after = normalized;
5520
5535
  if (!/require\(\s*['"]helmet['"]\s*\)|from\s+['"]helmet['"]/.test(after)) {
5521
5536
  after = `const helmet = require('helmet');
5522
5537
  ${after}`;
@@ -5525,17 +5540,19 @@ ${after}`;
5525
5540
  after = after.replace(/const app = express\(\s*\);/, (m) => `${m}
5526
5541
  app.use(helmet());`);
5527
5542
  }
5528
- if (after === source) return void 0;
5529
- return { kind: "config-add", safe: false, diff: unifiedDiff(file, source, after) };
5543
+ if (after === normalized) return void 0;
5544
+ return { kind: "config-add", safe: false, diff: unifiedDiff(file, normalized, after) };
5530
5545
  }
5531
5546
  function bodyLimitFix(file, source) {
5532
- if (!/express\.json\(\s*\)/.test(source)) return void 0;
5533
- const after = source.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5534
- return { kind: "config-add", safe: true, diff: unifiedDiff(file, source, after) };
5547
+ const normalized = normalizeNewlines(source);
5548
+ if (!/express\.json\(\s*\)/.test(normalized)) return void 0;
5549
+ const after = normalized.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5550
+ return { kind: "config-add", safe: true, diff: unifiedDiff(file, normalized, after) };
5535
5551
  }
5536
5552
  function cryptoFix(file, source) {
5537
- if (!/Math\.random\(\)/.test(source)) return void 0;
5538
- let after = source;
5553
+ const normalized = normalizeNewlines(source);
5554
+ if (!/Math\.random\(\)/.test(normalized)) return void 0;
5555
+ let after = normalized;
5539
5556
  if (!/require\(\s*['"]crypto['"]\s*\)|from\s+['"]crypto['"]/.test(after)) {
5540
5557
  after = `const crypto = require('crypto');
5541
5558
  ${after}`;
@@ -5544,21 +5561,23 @@ ${after}`;
5544
5561
  /Math\.random\(\)\.toString\(\s*36\s*\)(?:\.slice\(\s*2\s*\))?/g,
5545
5562
  "crypto.randomUUID()"
5546
5563
  );
5547
- if (after === source) return void 0;
5548
- return { kind: "code-patch", safe: true, diff: unifiedDiff(file, source, after) };
5564
+ if (after === normalized) return void 0;
5565
+ return { kind: "code-patch", safe: true, diff: unifiedDiff(file, normalized, after) };
5549
5566
  }
5550
5567
  function gitignoreFix(file, source) {
5551
5568
  const target = file.endsWith(".gitignore") ? file : ".gitignore";
5552
- if (/(^|[\r\n])\.env(\r?\n|$)/.test(source)) return void 0;
5553
- const after = source.length === 0 || source.endsWith("\n") || source.endsWith("\r") ? `${source}.env
5554
- ` : `${source}
5569
+ const normalized = normalizeNewlines(source);
5570
+ if (/(^|\n)\.env(\n|$)/.test(normalized)) return void 0;
5571
+ const after = normalized.length === 0 || normalized.endsWith("\n") ? `${normalized}.env
5572
+ ` : `${normalized}
5555
5573
  .env
5556
5574
  `;
5557
- return { kind: "config-add", safe: true, diff: unifiedDiff(target, source, after) };
5575
+ return { kind: "config-add", safe: true, diff: unifiedDiff(target, normalized, after) };
5558
5576
  }
5559
5577
  function secretEnvFix(file, source, finding) {
5560
5578
  const envName = envNameFor(finding.ruleId);
5561
- const lines = source.split("\n");
5579
+ const normalized = normalizeNewlines(source);
5580
+ const lines = normalized.split("\n");
5562
5581
  const idx = (finding.line ?? 1) - 1;
5563
5582
  const line = lines[idx];
5564
5583
  if (line === void 0) return void 0;
@@ -5569,7 +5588,7 @@ function secretEnvFix(file, source, finding) {
5569
5588
  if (replaced === line) return void 0;
5570
5589
  const afterLines = [...lines];
5571
5590
  afterLines[idx] = replaced;
5572
- return { kind: "code-patch", safe: false, diff: unifiedDiff(file, source, afterLines.join("\n")) };
5591
+ return { kind: "code-patch", safe: false, diff: unifiedDiff(file, normalized, afterLines.join("\n")) };
5573
5592
  }
5574
5593
  function envNameFor(ruleId) {
5575
5594
  if (ruleId.includes("AWS")) return "AWS_ACCESS_KEY_ID";
@@ -5583,9 +5602,9 @@ function envNameFor(ruleId) {
5583
5602
  return "SECRET";
5584
5603
  }
5585
5604
  function applyUnifiedDiff(source, diff) {
5586
- const plus = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5605
+ const plus = normalizeNewlines(diff).split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5587
5606
  if (plus.length === 0) return source;
5588
- return plus.join("\n");
5607
+ return applyEol(plus.join("\n"), detectEol(source));
5589
5608
  }
5590
5609
 
5591
5610
  // src/agent/index.ts
@@ -5606,6 +5625,9 @@ function assertLocalAgentApi(url) {
5606
5625
  function npmBin() {
5607
5626
  return process.platform === "win32" ? "npm.cmd" : "npm";
5608
5627
  }
5628
+ function gitPath(file) {
5629
+ return file.replace(/\\/g, "/");
5630
+ }
5609
5631
  function git(cwd, args) {
5610
5632
  return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
5611
5633
  }
@@ -5710,7 +5732,7 @@ async function runAgent(options = {}) {
5710
5732
  stoppedOnTestFailure = true;
5711
5733
  break;
5712
5734
  }
5713
- git(cwd, ["add", "--", ...files]);
5735
+ git(cwd, ["add", "--", ...files.map(gitPath)]);
5714
5736
  git(cwd, ["-c", "user.email=agent@fullshield.local", "-c", "user.name=FullShield Agent", "commit", "-m", `fix: ${finding.ruleId}`]);
5715
5737
  git(cwd, ["push", remote, `HEAD:${branch}`]);
5716
5738
  if (PROTECTED.has(branch)) {
package/dist/cli/index.js CHANGED
@@ -11,9 +11,6 @@ import { fileURLToPath } from 'url';
11
11
  import { performance } from 'perf_hooks';
12
12
  import { createRequire } from 'module';
13
13
  import { createServer } from 'http';
14
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
15
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
- import { z } from 'zod';
17
14
  import { Command } from 'commander';
18
15
  import { createConnection } from 'net';
19
16
  import pc from 'picocolors';
@@ -5898,11 +5895,29 @@ var init_engine2 = __esm({
5898
5895
  }
5899
5896
  });
5900
5897
 
5898
+ // src/fixer/eol.ts
5899
+ function detectEol(text) {
5900
+ return text.includes("\r\n") ? "\r\n" : "\n";
5901
+ }
5902
+ function normalizeNewlines(text) {
5903
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5904
+ }
5905
+ function applyEol(text, eol) {
5906
+ const normalized = normalizeNewlines(text);
5907
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
5908
+ }
5909
+ var init_eol = __esm({
5910
+ "src/fixer/eol.ts"() {
5911
+ }
5912
+ });
5913
+
5901
5914
  // src/fixer/diff.ts
5902
5915
  function unifiedDiff(filename, before, after) {
5903
- if (before === after) return "";
5904
- const oldLines = before.split("\n");
5905
- const newLines = after.split("\n");
5916
+ const oldNorm = normalizeNewlines(before);
5917
+ const newNorm = normalizeNewlines(after);
5918
+ if (oldNorm === newNorm) return "";
5919
+ const oldLines = oldNorm.split("\n");
5920
+ const newLines = newNorm.split("\n");
5906
5921
  const hunks = [`--- a/${filename}`, `+++ b/${filename}`];
5907
5922
  hunks.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);
5908
5923
  for (const line of oldLines) hunks.push(`-${line}`);
@@ -5911,6 +5926,7 @@ function unifiedDiff(filename, before, after) {
5911
5926
  }
5912
5927
  var init_diff = __esm({
5913
5928
  "src/fixer/diff.ts"() {
5929
+ init_eol();
5914
5930
  }
5915
5931
  });
5916
5932
 
@@ -5928,8 +5944,9 @@ function proposeFix(finding5, source) {
5928
5944
  return void 0;
5929
5945
  }
5930
5946
  function helmetFix(file, source) {
5931
- if (/helmet\s*\(/.test(source)) return void 0;
5932
- let after = source;
5947
+ const normalized = normalizeNewlines(source);
5948
+ if (/helmet\s*\(/.test(normalized)) return void 0;
5949
+ let after = normalized;
5933
5950
  if (!/require\(\s*['"]helmet['"]\s*\)|from\s+['"]helmet['"]/.test(after)) {
5934
5951
  after = `const helmet = require('helmet');
5935
5952
  ${after}`;
@@ -5938,17 +5955,19 @@ ${after}`;
5938
5955
  after = after.replace(/const app = express\(\s*\);/, (m) => `${m}
5939
5956
  app.use(helmet());`);
5940
5957
  }
5941
- if (after === source) return void 0;
5942
- return { kind: "config-add", safe: false, diff: unifiedDiff(file, source, after) };
5958
+ if (after === normalized) return void 0;
5959
+ return { kind: "config-add", safe: false, diff: unifiedDiff(file, normalized, after) };
5943
5960
  }
5944
5961
  function bodyLimitFix(file, source) {
5945
- if (!/express\.json\(\s*\)/.test(source)) return void 0;
5946
- const after = source.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5947
- return { kind: "config-add", safe: true, diff: unifiedDiff(file, source, after) };
5962
+ const normalized = normalizeNewlines(source);
5963
+ if (!/express\.json\(\s*\)/.test(normalized)) return void 0;
5964
+ const after = normalized.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5965
+ return { kind: "config-add", safe: true, diff: unifiedDiff(file, normalized, after) };
5948
5966
  }
5949
5967
  function cryptoFix(file, source) {
5950
- if (!/Math\.random\(\)/.test(source)) return void 0;
5951
- let after = source;
5968
+ const normalized = normalizeNewlines(source);
5969
+ if (!/Math\.random\(\)/.test(normalized)) return void 0;
5970
+ let after = normalized;
5952
5971
  if (!/require\(\s*['"]crypto['"]\s*\)|from\s+['"]crypto['"]/.test(after)) {
5953
5972
  after = `const crypto = require('crypto');
5954
5973
  ${after}`;
@@ -5957,21 +5976,23 @@ ${after}`;
5957
5976
  /Math\.random\(\)\.toString\(\s*36\s*\)(?:\.slice\(\s*2\s*\))?/g,
5958
5977
  "crypto.randomUUID()"
5959
5978
  );
5960
- if (after === source) return void 0;
5961
- return { kind: "code-patch", safe: true, diff: unifiedDiff(file, source, after) };
5979
+ if (after === normalized) return void 0;
5980
+ return { kind: "code-patch", safe: true, diff: unifiedDiff(file, normalized, after) };
5962
5981
  }
5963
5982
  function gitignoreFix(file, source) {
5964
5983
  const target = file.endsWith(".gitignore") ? file : ".gitignore";
5965
- if (/(^|[\r\n])\.env(\r?\n|$)/.test(source)) return void 0;
5966
- const after = source.length === 0 || source.endsWith("\n") || source.endsWith("\r") ? `${source}.env
5967
- ` : `${source}
5984
+ const normalized = normalizeNewlines(source);
5985
+ if (/(^|\n)\.env(\n|$)/.test(normalized)) return void 0;
5986
+ const after = normalized.length === 0 || normalized.endsWith("\n") ? `${normalized}.env
5987
+ ` : `${normalized}
5968
5988
  .env
5969
5989
  `;
5970
- return { kind: "config-add", safe: true, diff: unifiedDiff(target, source, after) };
5990
+ return { kind: "config-add", safe: true, diff: unifiedDiff(target, normalized, after) };
5971
5991
  }
5972
5992
  function secretEnvFix(file, source, finding5) {
5973
5993
  const envName = envNameFor(finding5.ruleId);
5974
- const lines = source.split("\n");
5994
+ const normalized = normalizeNewlines(source);
5995
+ const lines = normalized.split("\n");
5975
5996
  const idx = (finding5.line ?? 1) - 1;
5976
5997
  const line = lines[idx];
5977
5998
  if (line === void 0) return void 0;
@@ -5982,7 +6003,7 @@ function secretEnvFix(file, source, finding5) {
5982
6003
  if (replaced === line) return void 0;
5983
6004
  const afterLines = [...lines];
5984
6005
  afterLines[idx] = replaced;
5985
- return { kind: "code-patch", safe: false, diff: unifiedDiff(file, source, afterLines.join("\n")) };
6006
+ return { kind: "code-patch", safe: false, diff: unifiedDiff(file, normalized, afterLines.join("\n")) };
5986
6007
  }
5987
6008
  function envNameFor(ruleId) {
5988
6009
  if (ruleId.includes("AWS")) return "AWS_ACCESS_KEY_ID";
@@ -5996,13 +6017,14 @@ function envNameFor(ruleId) {
5996
6017
  return "SECRET";
5997
6018
  }
5998
6019
  function applyUnifiedDiff(source, diff) {
5999
- const plus = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
6020
+ const plus = normalizeNewlines(diff).split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
6000
6021
  if (plus.length === 0) return source;
6001
- return plus.join("\n");
6022
+ return applyEol(plus.join("\n"), detectEol(source));
6002
6023
  }
6003
6024
  var init_patches = __esm({
6004
6025
  "src/fixer/patches.ts"() {
6005
6026
  init_diff();
6027
+ init_eol();
6006
6028
  }
6007
6029
  });
6008
6030
 
@@ -6365,23 +6387,39 @@ __export(dashboard_exports, {
6365
6387
  });
6366
6388
  function sqliteDb(file) {
6367
6389
  const { DatabaseSync } = require2("node:sqlite");
6368
- const db = new DatabaseSync(file);
6369
- db.exec(
6370
- "CREATE TABLE IF NOT EXISTS scans (scannedAt TEXT NOT NULL, total INTEGER NOT NULL, highCrit INTEGER NOT NULL)"
6371
- );
6390
+ let db;
6391
+ try {
6392
+ db = new DatabaseSync(file);
6393
+ db.exec(
6394
+ "CREATE TABLE IF NOT EXISTS scans (scannedAt TEXT NOT NULL, total INTEGER NOT NULL, highCrit INTEGER NOT NULL)"
6395
+ );
6396
+ } catch (error) {
6397
+ try {
6398
+ db?.close();
6399
+ } catch {
6400
+ }
6401
+ throw error;
6402
+ }
6403
+ let closed = false;
6404
+ const handle = db;
6372
6405
  return {
6373
6406
  list() {
6374
- return db.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
6407
+ return handle.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
6375
6408
  },
6376
6409
  insert(row) {
6377
- db.prepare("INSERT INTO scans (scannedAt, total, highCrit) VALUES (?, ?, ?)").run(
6410
+ handle.prepare("INSERT INTO scans (scannedAt, total, highCrit) VALUES (?, ?, ?)").run(
6378
6411
  row.scannedAt,
6379
6412
  row.total,
6380
6413
  row.highCrit
6381
6414
  );
6382
6415
  },
6383
6416
  close() {
6384
- db.close();
6417
+ if (closed) return;
6418
+ closed = true;
6419
+ try {
6420
+ handle.close();
6421
+ } catch {
6422
+ }
6385
6423
  }
6386
6424
  };
6387
6425
  }
@@ -6472,7 +6510,13 @@ function startDashboard(opts) {
6472
6510
  })
6473
6511
  });
6474
6512
  });
6475
- server.on("error", reject);
6513
+ server.on("error", (err) => {
6514
+ try {
6515
+ db.close();
6516
+ } catch {
6517
+ }
6518
+ reject(err);
6519
+ });
6476
6520
  });
6477
6521
  }
6478
6522
  var require2;
@@ -6582,11 +6626,13 @@ var init_llm = __esm({
6582
6626
  // src/mcp/index.ts
6583
6627
  var mcp_exports = {};
6584
6628
  __export(mcp_exports, {
6629
+ MCP_PEER_ERROR: () => MCP_PEER_ERROR,
6585
6630
  callMcpTool: () => callMcpTool,
6586
6631
  createMcpSession: () => createMcpSession,
6587
6632
  createOfficialMcpServer: () => createOfficialMcpServer,
6588
6633
  handleMcpRequest: () => handleMcpRequest,
6589
6634
  listMcpTools: () => listMcpTools,
6635
+ loadMcpPeers: () => loadMcpPeers,
6590
6636
  startMcpStdio: () => startMcpStdio
6591
6637
  });
6592
6638
  function createMcpSession(opts = {}) {
@@ -6680,7 +6726,15 @@ async function handleMcpRequest(session, req) {
6680
6726
  function textResult(value) {
6681
6727
  return { content: [{ type: "text", text: JSON.stringify(value) }] };
6682
6728
  }
6683
- function createOfficialMcpServer(opts = {}) {
6729
+ async function loadMcpPeers(importer = defaultMcpPeerImporter) {
6730
+ try {
6731
+ return await importer();
6732
+ } catch {
6733
+ throw new Error(MCP_PEER_ERROR);
6734
+ }
6735
+ }
6736
+ async function createOfficialMcpServer(opts = {}) {
6737
+ const { McpServer, z } = await loadMcpPeers(opts.loadPeers);
6684
6738
  const session = createMcpSession(opts);
6685
6739
  const server = new McpServer({ name: "fullshield", version: "0.1.0" });
6686
6740
  const modulesSchema = z.array(z.enum(["deps", "secrets", "sast", "config"])).optional();
@@ -6728,15 +6782,30 @@ function createOfficialMcpServer(opts = {}) {
6728
6782
  return server;
6729
6783
  }
6730
6784
  async function startMcpStdio(opts = {}) {
6731
- const server = createOfficialMcpServer(opts);
6785
+ const { StdioServerTransport } = await loadMcpPeers(opts.loadPeers);
6786
+ const server = await createOfficialMcpServer(opts);
6732
6787
  const transport = new StdioServerTransport();
6733
6788
  await server.connect(transport);
6734
6789
  }
6790
+ var MCP_PEER_ERROR, defaultMcpPeerImporter;
6735
6791
  var init_mcp = __esm({
6736
6792
  "src/mcp/index.ts"() {
6737
6793
  init_engine2();
6738
6794
  init_patches();
6739
6795
  init_llm();
6796
+ MCP_PEER_ERROR = "fullshield mcp requires optional peers @modelcontextprotocol/sdk and zod. Install with: npm i -D @modelcontextprotocol/sdk zod";
6797
+ defaultMcpPeerImporter = async () => {
6798
+ const [serverMod, stdioMod, zodMod] = await Promise.all([
6799
+ import('@modelcontextprotocol/sdk/server/mcp.js'),
6800
+ import('@modelcontextprotocol/sdk/server/stdio.js'),
6801
+ import('zod')
6802
+ ]);
6803
+ return {
6804
+ McpServer: serverMod.McpServer,
6805
+ StdioServerTransport: stdioMod.StdioServerTransport,
6806
+ z: zodMod.z
6807
+ };
6808
+ };
6740
6809
  }
6741
6810
  });
6742
6811
 
@@ -7155,6 +7224,9 @@ function assertLocalAgentApi(url) {
7155
7224
  function npmBin() {
7156
7225
  return process.platform === "win32" ? "npm.cmd" : "npm";
7157
7226
  }
7227
+ function gitPath(file) {
7228
+ return file.replace(/\\/g, "/");
7229
+ }
7158
7230
  function git(cwd, args) {
7159
7231
  return execFileSync("git", args, { cwd, encoding: "utf8" }).trim();
7160
7232
  }
@@ -7259,7 +7331,7 @@ async function runAgent(options = {}) {
7259
7331
  stoppedOnTestFailure = true;
7260
7332
  break;
7261
7333
  }
7262
- git(cwd, ["add", "--", ...files]);
7334
+ git(cwd, ["add", "--", ...files.map(gitPath)]);
7263
7335
  git(cwd, ["-c", "user.email=agent@fullshield.local", "-c", "user.name=FullShield Agent", "commit", "-m", `fix: ${finding5.ruleId}`]);
7264
7336
  git(cwd, ["push", remote, `HEAD:${branch}`]);
7265
7337
  if (PROTECTED.has(branch)) {
package/dist/dashboard.js CHANGED
@@ -7,23 +7,39 @@ import { join } from 'path';
7
7
  var require2 = createRequire(import.meta.url);
8
8
  function sqliteDb(file) {
9
9
  const { DatabaseSync } = require2("node:sqlite");
10
- const db = new DatabaseSync(file);
11
- db.exec(
12
- "CREATE TABLE IF NOT EXISTS scans (scannedAt TEXT NOT NULL, total INTEGER NOT NULL, highCrit INTEGER NOT NULL)"
13
- );
10
+ let db;
11
+ try {
12
+ db = new DatabaseSync(file);
13
+ db.exec(
14
+ "CREATE TABLE IF NOT EXISTS scans (scannedAt TEXT NOT NULL, total INTEGER NOT NULL, highCrit INTEGER NOT NULL)"
15
+ );
16
+ } catch (error) {
17
+ try {
18
+ db?.close();
19
+ } catch {
20
+ }
21
+ throw error;
22
+ }
23
+ let closed = false;
24
+ const handle = db;
14
25
  return {
15
26
  list() {
16
- return db.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
27
+ return handle.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
17
28
  },
18
29
  insert(row) {
19
- db.prepare("INSERT INTO scans (scannedAt, total, highCrit) VALUES (?, ?, ?)").run(
30
+ handle.prepare("INSERT INTO scans (scannedAt, total, highCrit) VALUES (?, ?, ?)").run(
20
31
  row.scannedAt,
21
32
  row.total,
22
33
  row.highCrit
23
34
  );
24
35
  },
25
36
  close() {
26
- db.close();
37
+ if (closed) return;
38
+ closed = true;
39
+ try {
40
+ handle.close();
41
+ } catch {
42
+ }
27
43
  }
28
44
  };
29
45
  }
@@ -114,7 +130,13 @@ function startDashboard(opts) {
114
130
  })
115
131
  });
116
132
  });
117
- server.on("error", reject);
133
+ server.on("error", (err) => {
134
+ try {
135
+ db.close();
136
+ } catch {
137
+ }
138
+ reject(err);
139
+ });
118
140
  });
119
141
  }
120
142
 
package/dist/index.js CHANGED
@@ -5558,11 +5558,25 @@ async function createFixBranch(cwd) {
5558
5558
  return name;
5559
5559
  }
5560
5560
 
5561
+ // src/fixer/eol.ts
5562
+ function detectEol(text) {
5563
+ return text.includes("\r\n") ? "\r\n" : "\n";
5564
+ }
5565
+ function normalizeNewlines(text) {
5566
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5567
+ }
5568
+ function applyEol(text, eol) {
5569
+ const normalized = normalizeNewlines(text);
5570
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
5571
+ }
5572
+
5561
5573
  // src/fixer/diff.ts
5562
5574
  function unifiedDiff(filename, before, after) {
5563
- if (before === after) return "";
5564
- const oldLines = before.split("\n");
5565
- const newLines = after.split("\n");
5575
+ const oldNorm = normalizeNewlines(before);
5576
+ const newNorm = normalizeNewlines(after);
5577
+ if (oldNorm === newNorm) return "";
5578
+ const oldLines = oldNorm.split("\n");
5579
+ const newLines = newNorm.split("\n");
5566
5580
  const hunks = [`--- a/${filename}`, `+++ b/${filename}`];
5567
5581
  hunks.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);
5568
5582
  for (const line of oldLines) hunks.push(`-${line}`);
@@ -5584,8 +5598,9 @@ function proposeFix(finding, source) {
5584
5598
  return void 0;
5585
5599
  }
5586
5600
  function helmetFix(file, source) {
5587
- if (/helmet\s*\(/.test(source)) return void 0;
5588
- let after = source;
5601
+ const normalized = normalizeNewlines(source);
5602
+ if (/helmet\s*\(/.test(normalized)) return void 0;
5603
+ let after = normalized;
5589
5604
  if (!/require\(\s*['"]helmet['"]\s*\)|from\s+['"]helmet['"]/.test(after)) {
5590
5605
  after = `const helmet = require('helmet');
5591
5606
  ${after}`;
@@ -5594,17 +5609,19 @@ ${after}`;
5594
5609
  after = after.replace(/const app = express\(\s*\);/, (m) => `${m}
5595
5610
  app.use(helmet());`);
5596
5611
  }
5597
- if (after === source) return void 0;
5598
- return { kind: "config-add", safe: false, diff: unifiedDiff(file, source, after) };
5612
+ if (after === normalized) return void 0;
5613
+ return { kind: "config-add", safe: false, diff: unifiedDiff(file, normalized, after) };
5599
5614
  }
5600
5615
  function bodyLimitFix(file, source) {
5601
- if (!/express\.json\(\s*\)/.test(source)) return void 0;
5602
- const after = source.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5603
- return { kind: "config-add", safe: true, diff: unifiedDiff(file, source, after) };
5616
+ const normalized = normalizeNewlines(source);
5617
+ if (!/express\.json\(\s*\)/.test(normalized)) return void 0;
5618
+ const after = normalized.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5619
+ return { kind: "config-add", safe: true, diff: unifiedDiff(file, normalized, after) };
5604
5620
  }
5605
5621
  function cryptoFix(file, source) {
5606
- if (!/Math\.random\(\)/.test(source)) return void 0;
5607
- let after = source;
5622
+ const normalized = normalizeNewlines(source);
5623
+ if (!/Math\.random\(\)/.test(normalized)) return void 0;
5624
+ let after = normalized;
5608
5625
  if (!/require\(\s*['"]crypto['"]\s*\)|from\s+['"]crypto['"]/.test(after)) {
5609
5626
  after = `const crypto = require('crypto');
5610
5627
  ${after}`;
@@ -5613,21 +5630,23 @@ ${after}`;
5613
5630
  /Math\.random\(\)\.toString\(\s*36\s*\)(?:\.slice\(\s*2\s*\))?/g,
5614
5631
  "crypto.randomUUID()"
5615
5632
  );
5616
- if (after === source) return void 0;
5617
- return { kind: "code-patch", safe: true, diff: unifiedDiff(file, source, after) };
5633
+ if (after === normalized) return void 0;
5634
+ return { kind: "code-patch", safe: true, diff: unifiedDiff(file, normalized, after) };
5618
5635
  }
5619
5636
  function gitignoreFix(file, source) {
5620
5637
  const target = file.endsWith(".gitignore") ? file : ".gitignore";
5621
- if (/(^|[\r\n])\.env(\r?\n|$)/.test(source)) return void 0;
5622
- const after = source.length === 0 || source.endsWith("\n") || source.endsWith("\r") ? `${source}.env
5623
- ` : `${source}
5638
+ const normalized = normalizeNewlines(source);
5639
+ if (/(^|\n)\.env(\n|$)/.test(normalized)) return void 0;
5640
+ const after = normalized.length === 0 || normalized.endsWith("\n") ? `${normalized}.env
5641
+ ` : `${normalized}
5624
5642
  .env
5625
5643
  `;
5626
- return { kind: "config-add", safe: true, diff: unifiedDiff(target, source, after) };
5644
+ return { kind: "config-add", safe: true, diff: unifiedDiff(target, normalized, after) };
5627
5645
  }
5628
5646
  function secretEnvFix(file, source, finding) {
5629
5647
  const envName = envNameFor(finding.ruleId);
5630
- const lines = source.split("\n");
5648
+ const normalized = normalizeNewlines(source);
5649
+ const lines = normalized.split("\n");
5631
5650
  const idx = (finding.line ?? 1) - 1;
5632
5651
  const line = lines[idx];
5633
5652
  if (line === void 0) return void 0;
@@ -5638,7 +5657,7 @@ function secretEnvFix(file, source, finding) {
5638
5657
  if (replaced === line) return void 0;
5639
5658
  const afterLines = [...lines];
5640
5659
  afterLines[idx] = replaced;
5641
- return { kind: "code-patch", safe: false, diff: unifiedDiff(file, source, afterLines.join("\n")) };
5660
+ return { kind: "code-patch", safe: false, diff: unifiedDiff(file, normalized, afterLines.join("\n")) };
5642
5661
  }
5643
5662
  function envNameFor(ruleId) {
5644
5663
  if (ruleId.includes("AWS")) return "AWS_ACCESS_KEY_ID";
@@ -5652,9 +5671,9 @@ function envNameFor(ruleId) {
5652
5671
  return "SECRET";
5653
5672
  }
5654
5673
  function applyUnifiedDiff(source, diff) {
5655
- const plus = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5674
+ const plus = normalizeNewlines(diff).split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5656
5675
  if (plus.length === 0) return source;
5657
- return plus.join("\n");
5676
+ return applyEol(plus.join("\n"), detectEol(source));
5658
5677
  }
5659
5678
 
5660
5679
  // src/fixer/index.ts
package/dist/mcp.js CHANGED
@@ -2,9 +2,6 @@ import { execFile, spawnSync } from 'child_process';
2
2
  import { readFileSync, statSync, mkdtempSync, cpSync, writeFileSync, rmSync, existsSync, readdirSync } from 'fs';
3
3
  import { homedir, tmpdir } from 'os';
4
4
  import { resolve, join, basename, dirname, posix, relative } from 'path';
5
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
- import { z } from 'zod';
8
5
  import { readFile, mkdir, writeFile, stat, readdir } from 'fs/promises';
9
6
  import { performance } from 'perf_hooks';
10
7
  import { createHash } from 'crypto';
@@ -5492,11 +5489,25 @@ function scan(options = {}) {
5492
5489
  return runScan(options);
5493
5490
  }
5494
5491
 
5492
+ // src/fixer/eol.ts
5493
+ function detectEol(text) {
5494
+ return text.includes("\r\n") ? "\r\n" : "\n";
5495
+ }
5496
+ function normalizeNewlines(text) {
5497
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
5498
+ }
5499
+ function applyEol(text, eol) {
5500
+ const normalized = normalizeNewlines(text);
5501
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
5502
+ }
5503
+
5495
5504
  // src/fixer/diff.ts
5496
5505
  function unifiedDiff(filename, before, after) {
5497
- if (before === after) return "";
5498
- const oldLines = before.split("\n");
5499
- const newLines = after.split("\n");
5506
+ const oldNorm = normalizeNewlines(before);
5507
+ const newNorm = normalizeNewlines(after);
5508
+ if (oldNorm === newNorm) return "";
5509
+ const oldLines = oldNorm.split("\n");
5510
+ const newLines = newNorm.split("\n");
5500
5511
  const hunks = [`--- a/${filename}`, `+++ b/${filename}`];
5501
5512
  hunks.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);
5502
5513
  for (const line of oldLines) hunks.push(`-${line}`);
@@ -5518,8 +5529,9 @@ function proposeFix(finding2, source) {
5518
5529
  return void 0;
5519
5530
  }
5520
5531
  function helmetFix(file, source) {
5521
- if (/helmet\s*\(/.test(source)) return void 0;
5522
- let after = source;
5532
+ const normalized = normalizeNewlines(source);
5533
+ if (/helmet\s*\(/.test(normalized)) return void 0;
5534
+ let after = normalized;
5523
5535
  if (!/require\(\s*['"]helmet['"]\s*\)|from\s+['"]helmet['"]/.test(after)) {
5524
5536
  after = `const helmet = require('helmet');
5525
5537
  ${after}`;
@@ -5528,17 +5540,19 @@ ${after}`;
5528
5540
  after = after.replace(/const app = express\(\s*\);/, (m) => `${m}
5529
5541
  app.use(helmet());`);
5530
5542
  }
5531
- if (after === source) return void 0;
5532
- return { kind: "config-add", safe: false, diff: unifiedDiff(file, source, after) };
5543
+ if (after === normalized) return void 0;
5544
+ return { kind: "config-add", safe: false, diff: unifiedDiff(file, normalized, after) };
5533
5545
  }
5534
5546
  function bodyLimitFix(file, source) {
5535
- if (!/express\.json\(\s*\)/.test(source)) return void 0;
5536
- const after = source.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5537
- return { kind: "config-add", safe: true, diff: unifiedDiff(file, source, after) };
5547
+ const normalized = normalizeNewlines(source);
5548
+ if (!/express\.json\(\s*\)/.test(normalized)) return void 0;
5549
+ const after = normalized.replace(/express\.json\(\s*\)/g, "express.json({ limit: '100kb' })");
5550
+ return { kind: "config-add", safe: true, diff: unifiedDiff(file, normalized, after) };
5538
5551
  }
5539
5552
  function cryptoFix(file, source) {
5540
- if (!/Math\.random\(\)/.test(source)) return void 0;
5541
- let after = source;
5553
+ const normalized = normalizeNewlines(source);
5554
+ if (!/Math\.random\(\)/.test(normalized)) return void 0;
5555
+ let after = normalized;
5542
5556
  if (!/require\(\s*['"]crypto['"]\s*\)|from\s+['"]crypto['"]/.test(after)) {
5543
5557
  after = `const crypto = require('crypto');
5544
5558
  ${after}`;
@@ -5547,21 +5561,23 @@ ${after}`;
5547
5561
  /Math\.random\(\)\.toString\(\s*36\s*\)(?:\.slice\(\s*2\s*\))?/g,
5548
5562
  "crypto.randomUUID()"
5549
5563
  );
5550
- if (after === source) return void 0;
5551
- return { kind: "code-patch", safe: true, diff: unifiedDiff(file, source, after) };
5564
+ if (after === normalized) return void 0;
5565
+ return { kind: "code-patch", safe: true, diff: unifiedDiff(file, normalized, after) };
5552
5566
  }
5553
5567
  function gitignoreFix(file, source) {
5554
5568
  const target = file.endsWith(".gitignore") ? file : ".gitignore";
5555
- if (/(^|[\r\n])\.env(\r?\n|$)/.test(source)) return void 0;
5556
- const after = source.length === 0 || source.endsWith("\n") || source.endsWith("\r") ? `${source}.env
5557
- ` : `${source}
5569
+ const normalized = normalizeNewlines(source);
5570
+ if (/(^|\n)\.env(\n|$)/.test(normalized)) return void 0;
5571
+ const after = normalized.length === 0 || normalized.endsWith("\n") ? `${normalized}.env
5572
+ ` : `${normalized}
5558
5573
  .env
5559
5574
  `;
5560
- return { kind: "config-add", safe: true, diff: unifiedDiff(target, source, after) };
5575
+ return { kind: "config-add", safe: true, diff: unifiedDiff(target, normalized, after) };
5561
5576
  }
5562
5577
  function secretEnvFix(file, source, finding2) {
5563
5578
  const envName = envNameFor(finding2.ruleId);
5564
- const lines = source.split("\n");
5579
+ const normalized = normalizeNewlines(source);
5580
+ const lines = normalized.split("\n");
5565
5581
  const idx = (finding2.line ?? 1) - 1;
5566
5582
  const line = lines[idx];
5567
5583
  if (line === void 0) return void 0;
@@ -5572,7 +5588,7 @@ function secretEnvFix(file, source, finding2) {
5572
5588
  if (replaced === line) return void 0;
5573
5589
  const afterLines = [...lines];
5574
5590
  afterLines[idx] = replaced;
5575
- return { kind: "code-patch", safe: false, diff: unifiedDiff(file, source, afterLines.join("\n")) };
5591
+ return { kind: "code-patch", safe: false, diff: unifiedDiff(file, normalized, afterLines.join("\n")) };
5576
5592
  }
5577
5593
  function envNameFor(ruleId) {
5578
5594
  if (ruleId.includes("AWS")) return "AWS_ACCESS_KEY_ID";
@@ -5586,9 +5602,9 @@ function envNameFor(ruleId) {
5586
5602
  return "SECRET";
5587
5603
  }
5588
5604
  function applyUnifiedDiff(source, diff) {
5589
- const plus = diff.split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5605
+ const plus = normalizeNewlines(diff).split("\n").filter((line) => line.startsWith("+") && !line.startsWith("+++")).map((line) => line.slice(1));
5590
5606
  if (plus.length === 0) return source;
5591
- return plus.join("\n");
5607
+ return applyEol(plus.join("\n"), detectEol(source));
5592
5608
  }
5593
5609
  function finding(ruleId, title, message, file, severity = "high") {
5594
5610
  return {
@@ -5775,7 +5791,28 @@ async function handleMcpRequest(session, req) {
5775
5791
  function textResult(value) {
5776
5792
  return { content: [{ type: "text", text: JSON.stringify(value) }] };
5777
5793
  }
5778
- function createOfficialMcpServer(opts = {}) {
5794
+ var MCP_PEER_ERROR = "fullshield mcp requires optional peers @modelcontextprotocol/sdk and zod. Install with: npm i -D @modelcontextprotocol/sdk zod";
5795
+ var defaultMcpPeerImporter = async () => {
5796
+ const [serverMod, stdioMod, zodMod] = await Promise.all([
5797
+ import('@modelcontextprotocol/sdk/server/mcp.js'),
5798
+ import('@modelcontextprotocol/sdk/server/stdio.js'),
5799
+ import('zod')
5800
+ ]);
5801
+ return {
5802
+ McpServer: serverMod.McpServer,
5803
+ StdioServerTransport: stdioMod.StdioServerTransport,
5804
+ z: zodMod.z
5805
+ };
5806
+ };
5807
+ async function loadMcpPeers(importer = defaultMcpPeerImporter) {
5808
+ try {
5809
+ return await importer();
5810
+ } catch {
5811
+ throw new Error(MCP_PEER_ERROR);
5812
+ }
5813
+ }
5814
+ async function createOfficialMcpServer(opts = {}) {
5815
+ const { McpServer, z } = await loadMcpPeers(opts.loadPeers);
5779
5816
  const session = createMcpSession(opts);
5780
5817
  const server = new McpServer({ name: "fullshield", version: "0.1.0" });
5781
5818
  const modulesSchema = z.array(z.enum(["deps", "secrets", "sast", "config"])).optional();
@@ -5823,9 +5860,10 @@ function createOfficialMcpServer(opts = {}) {
5823
5860
  return server;
5824
5861
  }
5825
5862
  async function startMcpStdio(opts = {}) {
5826
- const server = createOfficialMcpServer(opts);
5863
+ const { StdioServerTransport } = await loadMcpPeers(opts.loadPeers);
5864
+ const server = await createOfficialMcpServer(opts);
5827
5865
  const transport = new StdioServerTransport();
5828
5866
  await server.connect(transport);
5829
5867
  }
5830
5868
 
5831
- export { callMcpTool, createMcpSession, createOfficialMcpServer, handleMcpRequest, listMcpTools, startMcpStdio };
5869
+ export { MCP_PEER_ERROR, callMcpTool, createMcpSession, createOfficialMcpServer, handleMcpRequest, listMcpTools, loadMcpPeers, startMcpStdio };
@@ -74,7 +74,20 @@ function createRateLimiter(options) {
74
74
  return {
75
75
  allow(key, now = Date.now()) {
76
76
  const windowStart = now - options.windowMs;
77
- const prev = (hits.get(key) ?? []).filter((t) => t > windowStart);
77
+ const prev = hits.get(key);
78
+ if (prev === void 0) {
79
+ hits.set(key, [now]);
80
+ return true;
81
+ }
82
+ let keep = 0;
83
+ for (let i = 0; i < prev.length; i += 1) {
84
+ const t = prev[i];
85
+ if (t !== void 0 && t > windowStart) {
86
+ prev[keep] = t;
87
+ keep += 1;
88
+ }
89
+ }
90
+ prev.length = keep;
78
91
  if (prev.length >= options.max) {
79
92
  hits.set(key, prev);
80
93
  return false;
@@ -74,7 +74,20 @@ function createRateLimiter(options) {
74
74
  return {
75
75
  allow(key, now = Date.now()) {
76
76
  const windowStart = now - options.windowMs;
77
- const prev = (hits.get(key) ?? []).filter((t) => t > windowStart);
77
+ const prev = hits.get(key);
78
+ if (prev === void 0) {
79
+ hits.set(key, [now]);
80
+ return true;
81
+ }
82
+ let keep = 0;
83
+ for (let i = 0; i < prev.length; i += 1) {
84
+ const t = prev[i];
85
+ if (t !== void 0 && t > windowStart) {
86
+ prev[keep] = t;
87
+ keep += 1;
88
+ }
89
+ }
90
+ prev.length = keep;
78
91
  if (prev.length >= options.max) {
79
92
  hits.set(key, prev);
80
93
  return false;
@@ -74,7 +74,20 @@ function createRateLimiter(options) {
74
74
  return {
75
75
  allow(key, now = Date.now()) {
76
76
  const windowStart = now - options.windowMs;
77
- const prev = (hits.get(key) ?? []).filter((t) => t > windowStart);
77
+ const prev = hits.get(key);
78
+ if (prev === void 0) {
79
+ hits.set(key, [now]);
80
+ return true;
81
+ }
82
+ let keep = 0;
83
+ for (let i = 0; i < prev.length; i += 1) {
84
+ const t = prev[i];
85
+ if (t !== void 0 && t > windowStart) {
86
+ prev[keep] = t;
87
+ keep += 1;
88
+ }
89
+ }
90
+ prev.length = keep;
78
91
  if (prev.length >= options.max) {
79
92
  hits.set(key, prev);
80
93
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullshield",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -32,7 +32,9 @@
32
32
  },
33
33
  "peerDependencies": {
34
34
  "express": "*",
35
- "fastify": "*"
35
+ "fastify": "*",
36
+ "@modelcontextprotocol/sdk": "^1.32.1",
37
+ "zod": "^4.6.7"
36
38
  },
37
39
  "peerDependenciesMeta": {
38
40
  "express": {
@@ -40,6 +42,12 @@
40
42
  },
41
43
  "fastify": {
42
44
  "optional": true
45
+ },
46
+ "@modelcontextprotocol/sdk": {
47
+ "optional": true
48
+ },
49
+ "zod": {
50
+ "optional": true
43
51
  }
44
52
  },
45
53
  "files": [
@@ -68,14 +76,13 @@
68
76
  "verify:phase": "node scripts/verify.mjs --phase"
69
77
  },
70
78
  "dependencies": {
71
- "@modelcontextprotocol/sdk": "^1.32.1",
72
79
  "@typescript-eslint/typescript-estree": "^8.71.2",
73
80
  "commander": "^12.1.0",
74
81
  "ignore": "^5.3.2",
75
- "picocolors": "^1.1.1",
76
- "zod": "^4.6.7"
82
+ "picocolors": "^1.1.1"
77
83
  },
78
84
  "devDependencies": {
85
+ "@modelcontextprotocol/sdk": "^1.32.1",
79
86
  "@eslint/js": "^9.14.0",
80
87
  "@types/node": "^22.9.0",
81
88
  "@vitest/coverage-v8": "^2.1.4",
@@ -85,6 +92,7 @@
85
92
  "tsup": "^8.3.5",
86
93
  "typescript": "^5.6.3",
87
94
  "typescript-eslint": "^8.13.0",
88
- "vitest": "^2.1.4"
95
+ "vitest": "^2.1.4",
96
+ "zod": "^4.6.7"
89
97
  }
90
98
  }
@@ -5,10 +5,10 @@ on:
5
5
  workflow_dispatch:
6
6
  jobs:
7
7
  watch:
8
- runs-on: ubuntu-latest
8
+ runs-on: ubuntu-24.04
9
9
  steps:
10
- - uses: actions/checkout@v4
11
- - uses: actions/setup-node@v4
10
+ - uses: actions/checkout@v7
11
+ - uses: actions/setup-node@v7
12
12
  with:
13
13
  node-version: '22'
14
14
  - run: npm ci
@@ -5,10 +5,10 @@ on:
5
5
  branches: [main]
6
6
  jobs:
7
7
  scan:
8
- runs-on: ubuntu-latest
8
+ runs-on: ubuntu-24.04
9
9
  steps:
10
- - uses: actions/checkout@v4
11
- - uses: actions/setup-node@v4
10
+ - uses: actions/checkout@v7
11
+ - uses: actions/setup-node@v7
12
12
  with:
13
13
  node-version: '22'
14
14
  - run: npm ci