security-mcp 1.0.4 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +77 -21
  2. package/defaults/checklists/ai.json +25 -0
  3. package/defaults/checklists/api.json +27 -0
  4. package/defaults/checklists/infra.json +27 -0
  5. package/defaults/checklists/mobile.json +25 -0
  6. package/defaults/checklists/payments.json +25 -0
  7. package/defaults/checklists/web.json +30 -0
  8. package/defaults/control-catalog.json +549 -0
  9. package/defaults/evidence-map.json +194 -0
  10. package/defaults/security-exceptions.json +4 -0
  11. package/defaults/security-policy.json +41 -2
  12. package/defaults/security-tools.json +41 -0
  13. package/dist/ci/pr-gate.js +2 -3
  14. package/dist/cli/index.js +63 -23
  15. package/dist/cli/install.js +47 -15
  16. package/dist/cli/onboarding.js +590 -0
  17. package/dist/cli/update.js +124 -0
  18. package/dist/gate/baseline.js +115 -0
  19. package/dist/gate/catalog.js +55 -0
  20. package/dist/gate/checks/ai-redteam.js +374 -0
  21. package/dist/gate/checks/ai.js +45 -14
  22. package/dist/gate/checks/api.js +93 -0
  23. package/dist/gate/checks/crypto.js +153 -0
  24. package/dist/gate/checks/database.js +144 -0
  25. package/dist/gate/checks/dependencies.js +130 -0
  26. package/dist/gate/checks/dlp.js +153 -0
  27. package/dist/gate/checks/graphql.js +122 -0
  28. package/dist/gate/checks/infra.js +126 -12
  29. package/dist/gate/checks/k8s.js +190 -0
  30. package/dist/gate/checks/playbook.js +160 -0
  31. package/dist/gate/checks/runtime.js +263 -0
  32. package/dist/gate/checks/sbom.js +199 -0
  33. package/dist/gate/checks/scanners.js +450 -0
  34. package/dist/gate/checks/secrets.js +119 -27
  35. package/dist/gate/diff.js +2 -2
  36. package/dist/gate/evidence.js +116 -0
  37. package/dist/gate/exceptions.js +85 -0
  38. package/dist/gate/policy.js +189 -17
  39. package/dist/gate/threat-intel.js +157 -0
  40. package/dist/mcp/server.js +938 -9
  41. package/dist/repo/fs.js +10 -5
  42. package/dist/repo/search.js +13 -1
  43. package/dist/review/store.js +208 -0
  44. package/dist/tests/run.js +103 -0
  45. package/package.json +13 -3
  46. package/prompts/SECURITY_PROMPT.md +455 -1
  47. package/skills/senior-security-engineer/SKILL.md +81 -4
package/dist/repo/fs.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- const ROOT = process.cwd();
4
- // ROOT_PREFIX ensures /home/u/project-adjacent doesn't pass a startsWith check for /home/u/project
5
- const ROOT_PREFIX = ROOT.endsWith(path.sep) ? ROOT : ROOT + path.sep;
3
+ function getWorkspaceRoot() {
4
+ return process.cwd();
5
+ }
6
+ function getWorkspacePrefix(root) {
7
+ return root.endsWith(path.sep) ? root : root + path.sep;
8
+ }
6
9
  export async function readFileSafe(relPath) {
7
- const p = path.resolve(ROOT, relPath);
10
+ const root = getWorkspaceRoot();
11
+ const rootPrefix = getWorkspacePrefix(root);
12
+ const p = path.resolve(root, relPath);
8
13
  // Allow exact match to ROOT itself or any path strictly under it.
9
14
  // Using ROOT_PREFIX prevents the classic prefix-collision bypass
10
15
  // (e.g. /app-sibling matching /app as a prefix). CWE-22.
11
- if (p !== ROOT && !p.startsWith(ROOT_PREFIX)) {
16
+ if (p !== root && !p.startsWith(rootPrefix)) {
12
17
  throw new Error("Path traversal blocked");
13
18
  }
14
19
  return await readFile(p, "utf8");
@@ -42,7 +42,19 @@ function scanLines(file, lines, opts, re, matches) {
42
42
  export async function searchRepo(opts) {
43
43
  const files = await fg(["**/*.*"], {
44
44
  dot: true,
45
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
45
+ ignore: [
46
+ "**/node_modules/**",
47
+ "**/.git/**",
48
+ "**/dist/**",
49
+ "**/.claude/**",
50
+ // Exclude tool-internal files — they contain detection patterns and remediation
51
+ // examples that would trigger their own scanners (false positives in self-scan).
52
+ // When deployed as a package, these live in node_modules and are ignored naturally.
53
+ "src/gate/**",
54
+ "src/mcp/**",
55
+ "src/cli/**",
56
+ "prompts/**"
57
+ ]
46
58
  });
47
59
  const re = opts.isRegex ? compileUserRegex(opts.query) : null;
48
60
  const matches = [];
@@ -0,0 +1,208 @@
1
+ import { createHash, createHmac, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const REVIEW_DIR = path.join(".mcp", "reviews");
5
+ const REPORT_DIR = path.join(".mcp", "reports");
6
+ const CHECKLIST_DEFAULTS_DIR = path.join(path.dirname(path.dirname(path.dirname(new URL(import.meta.url).pathname))), "defaults", "checklists");
7
+ async function ensureDir(dirPath) {
8
+ await mkdir(dirPath, { recursive: true });
9
+ }
10
+ function reviewPath(runId) {
11
+ return path.join(process.cwd(), REVIEW_DIR, `${runId}.json`);
12
+ }
13
+ function reportPath(runId) {
14
+ return path.join(process.cwd(), REPORT_DIR, `${runId}.attestation.json`);
15
+ }
16
+ async function writeJson(filePath, value) {
17
+ await ensureDir(path.dirname(filePath));
18
+ await writeFile(filePath, JSON.stringify(value, null, 2) + "\n", "utf-8");
19
+ }
20
+ function checklistPath(runId) {
21
+ return path.join(process.cwd(), REVIEW_DIR, `${runId}-checklist.json`);
22
+ }
23
+ async function readChecklistRaw(runId) {
24
+ try {
25
+ const raw = await readFile(checklistPath(runId), "utf-8");
26
+ return JSON.parse(raw);
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function computeAllCriticalComplete(items) {
33
+ return items
34
+ .filter((i) => i.critical)
35
+ .every((i) => i.status === "completed" || i.status === "na");
36
+ }
37
+ /**
38
+ * Initialize a checklist for a run from the surface template.
39
+ */
40
+ export async function initChecklist(runId, surface) {
41
+ // Load template from defaults/checklists/{surface}.json
42
+ let template;
43
+ try {
44
+ const raw = await readFile(path.join(CHECKLIST_DEFAULTS_DIR, `${surface}.json`), "utf-8");
45
+ template = JSON.parse(raw);
46
+ }
47
+ catch {
48
+ // Fallback to empty template
49
+ template = { surface, items: [] };
50
+ }
51
+ const items = template.items.map((item) => ({
52
+ id: item.id,
53
+ surface,
54
+ description: item.description,
55
+ critical: item.critical,
56
+ status: "pending",
57
+ runId
58
+ }));
59
+ const state = {
60
+ runId,
61
+ surface,
62
+ items,
63
+ allCriticalComplete: false
64
+ };
65
+ await writeJson(checklistPath(runId), state);
66
+ return state;
67
+ }
68
+ /**
69
+ * Mark a checklist item as completed.
70
+ */
71
+ export async function completeChecklistItem(runId, itemId, completedBy, evidence) {
72
+ const state = await readChecklistRaw(runId);
73
+ if (!state)
74
+ throw new Error(`No checklist found for runId: ${runId}`);
75
+ const item = state.items.find((i) => i.id === itemId);
76
+ if (!item)
77
+ throw new Error(`Checklist item not found: ${itemId}`);
78
+ item.status = "completed";
79
+ item.completedBy = completedBy;
80
+ item.completedAt = new Date().toISOString();
81
+ if (evidence)
82
+ item.evidence = evidence;
83
+ state.allCriticalComplete = computeAllCriticalComplete(state.items);
84
+ await writeJson(checklistPath(runId), state);
85
+ return state;
86
+ }
87
+ /**
88
+ * Mark a checklist item as not applicable.
89
+ */
90
+ export async function markChecklistItemNA(runId, itemId, completedBy, reason) {
91
+ const state = await readChecklistRaw(runId);
92
+ if (!state)
93
+ throw new Error(`No checklist found for runId: ${runId}`);
94
+ const item = state.items.find((i) => i.id === itemId);
95
+ if (!item)
96
+ throw new Error(`Checklist item not found: ${itemId}`);
97
+ item.status = "na";
98
+ item.completedBy = completedBy;
99
+ item.completedAt = new Date().toISOString();
100
+ item.evidence = reason;
101
+ state.allCriticalComplete = computeAllCriticalComplete(state.items);
102
+ await writeJson(checklistPath(runId), state);
103
+ return state;
104
+ }
105
+ /**
106
+ * Mark a checklist item as failed.
107
+ */
108
+ export async function failChecklistItem(runId, itemId, completedBy, reason) {
109
+ const state = await readChecklistRaw(runId);
110
+ if (!state)
111
+ throw new Error(`No checklist found for runId: ${runId}`);
112
+ const item = state.items.find((i) => i.id === itemId);
113
+ if (!item)
114
+ throw new Error(`Checklist item not found: ${itemId}`);
115
+ item.status = "failed";
116
+ item.completedBy = completedBy;
117
+ item.completedAt = new Date().toISOString();
118
+ item.evidence = reason;
119
+ state.allCriticalComplete = computeAllCriticalComplete(state.items);
120
+ await writeJson(checklistPath(runId), state);
121
+ return state;
122
+ }
123
+ /**
124
+ * Sign off on a checklist. Requires all non-NA critical items to be completed.
125
+ */
126
+ export async function signOffChecklist(runId, signedOffBy) {
127
+ const state = await readChecklistRaw(runId);
128
+ if (!state)
129
+ throw new Error(`No checklist found for runId: ${runId}`);
130
+ const blockers = state.items.filter((i) => i.critical && (i.status === "pending" || i.status === "failed"));
131
+ if (blockers.length > 0) {
132
+ const list = blockers.map((b) => `${b.id}: ${b.description} (${b.status})`).join("; ");
133
+ throw new Error(`Cannot sign off: ${blockers.length} critical item(s) are not completed: ${list}`);
134
+ }
135
+ state.signedOffBy = signedOffBy;
136
+ state.signedOffAt = new Date().toISOString();
137
+ state.allCriticalComplete = true;
138
+ await writeJson(checklistPath(runId), state);
139
+ return state;
140
+ }
141
+ /**
142
+ * Read checklist state for a run.
143
+ */
144
+ export async function readChecklist(runId) {
145
+ return readChecklistRaw(runId);
146
+ }
147
+ export async function createReviewRun(opts) {
148
+ const now = new Date().toISOString();
149
+ const cleanTargets = (opts.targets ?? []).map((target) => target.trim()).filter(Boolean);
150
+ const run = {
151
+ id: randomUUID(),
152
+ createdAt: now,
153
+ updatedAt: now,
154
+ mode: opts.mode,
155
+ targets: cleanTargets,
156
+ baseRef: opts.baseRef,
157
+ headRef: opts.headRef,
158
+ requiredSteps: ["scan_strategy", "threat_model", "checklist", "run_pr_gate"],
159
+ steps: {
160
+ start_review: {
161
+ status: "completed",
162
+ updatedAt: now,
163
+ details: {
164
+ mode: opts.mode,
165
+ targets: cleanTargets,
166
+ baseRef: opts.baseRef,
167
+ headRef: opts.headRef
168
+ }
169
+ }
170
+ }
171
+ };
172
+ await writeJson(reviewPath(run.id), run);
173
+ return run;
174
+ }
175
+ export async function readReviewRun(runId) {
176
+ const raw = await readFile(reviewPath(runId), "utf-8");
177
+ return JSON.parse(raw);
178
+ }
179
+ export async function updateReviewStep(runId, step, status, details) {
180
+ const run = await readReviewRun(runId);
181
+ run.steps[step] = {
182
+ status,
183
+ updatedAt: new Date().toISOString(),
184
+ details
185
+ };
186
+ run.updatedAt = new Date().toISOString();
187
+ await writeJson(reviewPath(run.id), run);
188
+ return run;
189
+ }
190
+ export async function createReviewAttestation(runId, payload, signatureKey) {
191
+ const digestInput = JSON.stringify(payload);
192
+ const sha256 = createHash("sha256").update(digestInput).digest("hex");
193
+ const hmacSha256 = signatureKey
194
+ ? createHmac("sha256", signatureKey).update(digestInput).digest("hex")
195
+ : undefined;
196
+ await writeJson(reportPath(runId), {
197
+ ...payload,
198
+ integrity: {
199
+ sha256,
200
+ ...(hmacSha256 ? { hmacSha256 } : {})
201
+ }
202
+ });
203
+ return {
204
+ path: reportPath(runId),
205
+ sha256,
206
+ hmacSha256
207
+ };
208
+ }
@@ -0,0 +1,103 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, readFileSync, rmSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { runPrGate } from "../gate/policy.js";
5
+ import { createReviewAttestation, createReviewRun, readReviewRun, updateReviewStep } from "../review/store.js";
6
+ function repoPath(...parts) {
7
+ return path.join(process.cwd(), ...parts);
8
+ }
9
+ function cleanupFixtureReviewArtifacts(fixtureName) {
10
+ const fixtureRoot = repoPath("fixtures", fixtureName, ".mcp");
11
+ rmSync(path.join(fixtureRoot, "reports"), { recursive: true, force: true });
12
+ rmSync(path.join(fixtureRoot, "reviews"), { recursive: true, force: true });
13
+ }
14
+ async function withFixture(fixtureName, fn) {
15
+ const previous = process.cwd();
16
+ process.chdir(repoPath("fixtures", fixtureName));
17
+ try {
18
+ return await fn();
19
+ }
20
+ finally {
21
+ process.chdir(previous);
22
+ }
23
+ }
24
+ async function runPromptConformanceTests() {
25
+ const prompt = readFileSync(repoPath("prompts", "SECURITY_PROMPT.md"), "utf-8");
26
+ const skill = readFileSync(repoPath("skills", "senior-security-engineer", "SKILL.md"), "utf-8");
27
+ const readme = readFileSync(repoPath("README.md"), "utf-8");
28
+ const serverSource = readFileSync(repoPath("src", "mcp", "server.ts"), "utf-8");
29
+ assert.match(prompt, /security\.start_review/);
30
+ assert.match(prompt, /security\.attest_review/);
31
+ assert.match(prompt, /Human approval is mandatory/i);
32
+ assert.match(skill, /90% fixing/);
33
+ assert.match(skill, /security\.self_heal_loop/);
34
+ assert.match(readme, /security\.start_review/);
35
+ assert.match(readme, /security\.attest_review/);
36
+ assert.match(serverSource, /"security\.start_review"/);
37
+ assert.match(serverSource, /"security\.attest_review"/);
38
+ }
39
+ async function runFixtureGateTests() {
40
+ await withFixture("web-insecure", async () => {
41
+ const result = await runPrGate({
42
+ mode: "folder_by_folder",
43
+ targets: ["src"],
44
+ policyPath: ".mcp/policies/security-policy.json"
45
+ });
46
+ const ids = result.findings.map((finding) => finding.id);
47
+ assert.ok(ids.includes("WEB_HEADERS_MISSING"));
48
+ assert.ok(ids.includes("DANGEROUSLY_SET_INNER_HTML"));
49
+ assert.ok(ids.includes("SSRF_GUARD_REQUIRED"));
50
+ assert.ok(result.confidence);
51
+ });
52
+ await withFixture("infra-insecure", async () => {
53
+ const result = await runPrGate({
54
+ mode: "folder_by_folder",
55
+ targets: ["terraform"],
56
+ policyPath: ".mcp/policies/security-policy.json"
57
+ });
58
+ const ids = result.findings.map((finding) => finding.id);
59
+ assert.ok(ids.includes("PUBLIC_EXPOSURE_RISK"));
60
+ assert.ok(ids.includes("CONTROL_EVIDENCE_MISSING"));
61
+ });
62
+ await withFixture("ai-insecure", async () => {
63
+ const result = await runPrGate({
64
+ mode: "folder_by_folder",
65
+ targets: ["ai"],
66
+ policyPath: ".mcp/policies/security-policy.json"
67
+ });
68
+ const ids = result.findings.map((finding) => finding.id);
69
+ assert.ok(ids.includes("AI_OUTPUT_BOUNDS_MISSING"));
70
+ });
71
+ }
72
+ async function runReviewWorkflowTests() {
73
+ cleanupFixtureReviewArtifacts("web-insecure");
74
+ await withFixture("web-insecure", async () => {
75
+ const run = await createReviewRun({
76
+ mode: "folder_by_folder",
77
+ targets: ["src"]
78
+ });
79
+ await updateReviewStep(run.id, "scan_strategy", "completed", { mode: "folder_by_folder", targets: ["src"] });
80
+ await updateReviewStep(run.id, "threat_model", "completed", { feature: "fixture web flow" });
81
+ await updateReviewStep(run.id, "checklist", "completed", { surface: "web" });
82
+ await updateReviewStep(run.id, "run_pr_gate", "completed", { status: "FAIL", confidence: { score: 20 } });
83
+ const saved = await readReviewRun(run.id);
84
+ assert.equal(saved.steps["run_pr_gate"]?.status, "completed");
85
+ const attestation = await createReviewAttestation(run.id, {
86
+ runId: run.id,
87
+ steps: saved.steps
88
+ });
89
+ assert.ok(existsSync(attestation.path));
90
+ assert.match(attestation.sha256, /^[a-f0-9]{64}$/);
91
+ });
92
+ cleanupFixtureReviewArtifacts("web-insecure");
93
+ }
94
+ async function main() {
95
+ await runPromptConformanceTests();
96
+ await runFixtureGateTests();
97
+ await runReviewWorkflowTests();
98
+ console.log("security-mcp tests passed");
99
+ }
100
+ main().catch((error) => {
101
+ console.error(error);
102
+ process.exit(1);
103
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "security-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "AI security MCP server and enforcement gate for Claude Code, Cursor, GitHub Copilot, Codex, Replit, and any MCP-compatible editor. Applies OWASP, MITRE ATT&CK, NIST, Zero Trust, PCI DSS, SOC 2, and ISO 27001.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,21 +53,31 @@
53
53
  ],
54
54
  "scripts": {
55
55
  "build": "tsc -p tsconfig.json",
56
+ "lint": "eslint . --max-warnings=0",
56
57
  "prepublishOnly": "npm run build",
57
58
  "start": "node dist/cli/index.js serve",
58
59
  "mcp:server": "node dist/mcp/server.js",
59
- "ci:pr-gate": "node dist/ci/pr-gate.js"
60
+ "ci:pr-gate": "node dist/ci/pr-gate.js",
61
+ "test": "npm run build && node dist/tests/run.js"
60
62
  },
61
63
  "dependencies": {
62
- "@modelcontextprotocol/sdk": "^1.26.0",
64
+ "@modelcontextprotocol/sdk": "^1.27.1",
63
65
  "execa": "^9.5.2",
64
66
  "fast-glob": "^3.3.3",
65
67
  "picomatch": "^3.0.1",
66
68
  "zod": "^3.24.1"
67
69
  },
70
+ "overrides": {
71
+ "express-rate-limit": "^8.2.2",
72
+ "hono": "^4.12.7"
73
+ },
68
74
  "devDependencies": {
75
+ "@eslint/js": "^9.22.0",
69
76
  "@types/node": "^22.13.5",
70
77
  "@types/picomatch": "^2.3.4",
78
+ "eslint": "^9.22.0",
79
+ "globals": "^16.0.0",
80
+ "typescript-eslint": "^8.26.0",
71
81
  "typescript": "^5.7.3"
72
82
  },
73
83
  "engines": {