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 +13 -3
- package/README.md +6 -2
- package/dist/agent.js +45 -23
- package/dist/cli/index.js +108 -36
- package/dist/dashboard.js +30 -8
- package/dist/index.js +41 -22
- package/dist/mcp.js +66 -28
- package/dist/runtime/express.js +14 -1
- package/dist/runtime/fastify.js +14 -1
- package/dist/runtime/index.js +14 -1
- package/package.json +14 -6
- package/templates/github-watch.yml +3 -3
- package/templates/github-workflow.yml +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,19 @@ All notable changes to FullShield are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
5495
|
-
const
|
|
5496
|
-
|
|
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
|
-
|
|
5519
|
-
|
|
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 ===
|
|
5529
|
-
return { kind: "config-add", safe: false, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
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
|
-
|
|
5538
|
-
|
|
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 ===
|
|
5548
|
-
return { kind: "code-patch", safe: true, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
5904
|
-
const
|
|
5905
|
-
|
|
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
|
-
|
|
5932
|
-
|
|
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 ===
|
|
5942
|
-
return { kind: "config-add", safe: false, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
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
|
-
|
|
5951
|
-
|
|
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 ===
|
|
5961
|
-
return { kind: "code-patch", safe: true, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
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
|
|
6407
|
+
return handle.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
|
|
6375
6408
|
},
|
|
6376
6409
|
insert(row) {
|
|
6377
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
27
|
+
return handle.prepare("SELECT scannedAt, total, highCrit FROM scans ORDER BY scannedAt").all();
|
|
17
28
|
},
|
|
18
29
|
insert(row) {
|
|
19
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
5564
|
-
const
|
|
5565
|
-
|
|
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
|
-
|
|
5588
|
-
|
|
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 ===
|
|
5598
|
-
return { kind: "config-add", safe: false, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
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
|
-
|
|
5607
|
-
|
|
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 ===
|
|
5617
|
-
return { kind: "code-patch", safe: true, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
5498
|
-
const
|
|
5499
|
-
|
|
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
|
-
|
|
5522
|
-
|
|
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 ===
|
|
5532
|
-
return { kind: "config-add", safe: false, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
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
|
-
|
|
5541
|
-
|
|
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 ===
|
|
5551
|
-
return { kind: "code-patch", safe: true, diff: unifiedDiff(file,
|
|
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
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 };
|
package/dist/runtime/express.js
CHANGED
|
@@ -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 =
|
|
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/dist/runtime/fastify.js
CHANGED
|
@@ -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 =
|
|
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/dist/runtime/index.js
CHANGED
|
@@ -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 =
|
|
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.
|
|
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-
|
|
8
|
+
runs-on: ubuntu-24.04
|
|
9
9
|
steps:
|
|
10
|
-
- uses: actions/checkout@
|
|
11
|
-
- uses: actions/setup-node@
|
|
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-
|
|
8
|
+
runs-on: ubuntu-24.04
|
|
9
9
|
steps:
|
|
10
|
-
- uses: actions/checkout@
|
|
11
|
-
- uses: actions/setup-node@
|
|
10
|
+
- uses: actions/checkout@v7
|
|
11
|
+
- uses: actions/setup-node@v7
|
|
12
12
|
with:
|
|
13
13
|
node-version: '22'
|
|
14
14
|
- run: npm ci
|