security-mcp 1.1.0 → 1.1.2

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 (118) hide show
  1. package/README.md +966 -193
  2. package/defaults/agent-run-schema.json +98 -0
  3. package/dist/ci/pr-gate.js +18 -1
  4. package/dist/cli/install.js +69 -2
  5. package/dist/cli/onboarding.js +82 -11
  6. package/dist/cli/update.js +83 -15
  7. package/dist/gate/checks/ai-redteam.js +83 -59
  8. package/dist/gate/checks/api.js +93 -0
  9. package/dist/gate/checks/ci-pipeline.js +135 -0
  10. package/dist/gate/checks/crypto.js +91 -22
  11. package/dist/gate/checks/database.js +5 -1
  12. package/dist/gate/checks/dependencies.js +297 -2
  13. package/dist/gate/checks/dlp.js +6 -1
  14. package/dist/gate/checks/graphql.js +6 -1
  15. package/dist/gate/checks/k8s.js +229 -181
  16. package/dist/gate/checks/nuclei.js +133 -0
  17. package/dist/gate/checks/runtime.js +75 -8
  18. package/dist/gate/checks/scanners.js +8 -2
  19. package/dist/gate/diff.js +2 -0
  20. package/dist/gate/exceptions.js +6 -1
  21. package/dist/gate/policy.js +47 -4
  22. package/dist/gate/result.js +7 -1
  23. package/dist/mcp/audit-chain.js +253 -0
  24. package/dist/mcp/learning.js +228 -0
  25. package/dist/mcp/model-router.js +544 -0
  26. package/dist/mcp/orchestration.js +604 -0
  27. package/dist/mcp/server.js +160 -12
  28. package/dist/repo/search.js +5 -7
  29. package/dist/review/store.js +15 -0
  30. package/dist/types/agent-run.js +8 -0
  31. package/package.json +5 -5
  32. package/skills/_TEMPLATE/SKILL.md +99 -0
  33. package/skills/advanced-dos-tester/SKILL.md +225 -0
  34. package/skills/agentic-loop-exploiter/SKILL.md +69 -0
  35. package/skills/ai-llm-redteam/SKILL.md +118 -0
  36. package/skills/ai-model-supply-chain-agent/SKILL.md +198 -0
  37. package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
  38. package/skills/android-penetration-tester/SKILL.md +83 -0
  39. package/skills/anti-replay-tester/SKILL.md +195 -0
  40. package/skills/appsec-code-auditor/SKILL.md +86 -0
  41. package/skills/artifact-integrity-analyst/SKILL.md +68 -0
  42. package/skills/attack-navigator/SKILL.md +64 -0
  43. package/skills/auth-session-hacker/SKILL.md +87 -0
  44. package/skills/aws-penetration-tester/SKILL.md +60 -0
  45. package/skills/azure-penetration-tester/SKILL.md +64 -0
  46. package/skills/binary-auth-validator/SKILL.md +184 -0
  47. package/skills/bot-detection-specialist/SKILL.md +221 -0
  48. package/skills/business-logic-attacker/SKILL.md +76 -0
  49. package/skills/capec-code-mapper/SKILL.md +163 -0
  50. package/skills/cert-pin-rotation-specialist/SKILL.md +200 -0
  51. package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
  52. package/skills/ciso-orchestrator/SKILL.md +165 -0
  53. package/skills/cloud-infra-specialist/SKILL.md +85 -0
  54. package/skills/compliance-gap-analyst/SKILL.md +77 -0
  55. package/skills/compliance-grc/SKILL.md +148 -0
  56. package/skills/compliance-lifecycle-tracker/SKILL.md +169 -0
  57. package/skills/credential-stuffing-specialist/SKILL.md +192 -0
  58. package/skills/crypto-pki-specialist/SKILL.md +136 -0
  59. package/skills/csa-ccm-mapper/SKILL.md +178 -0
  60. package/skills/csf2-governance-mapper/SKILL.md +159 -0
  61. package/skills/deep-link-fuzzer/SKILL.md +195 -0
  62. package/skills/dependency-confusion-attacker/SKILL.md +78 -0
  63. package/skills/device-integrity-aggregator/SKILL.md +221 -0
  64. package/skills/dos-resilience-tester/SKILL.md +184 -0
  65. package/skills/dread-scorer/SKILL.md +157 -0
  66. package/skills/egress-policy-enforcer/SKILL.md +208 -0
  67. package/skills/evidence-collector/SKILL.md +86 -0
  68. package/skills/file-upload-attacker/SKILL.md +208 -0
  69. package/skills/gcp-penetration-tester/SKILL.md +63 -0
  70. package/skills/git-history-secret-scanner/SKILL.md +182 -0
  71. package/skills/iam-privesc-graph-builder/SKILL.md +216 -0
  72. package/skills/incident-responder/SKILL.md +192 -0
  73. package/skills/injection-specialist/SKILL.md +62 -0
  74. package/skills/ios-security-auditor/SKILL.md +77 -0
  75. package/skills/json-ambiguity-tester/SKILL.md +175 -0
  76. package/skills/k8s-container-escaper/SKILL.md +74 -0
  77. package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
  78. package/skills/kill-switch-engineer/SKILL.md +205 -0
  79. package/skills/linddun-privacy-analyst/SKILL.md +196 -0
  80. package/skills/logic-race-fuzzer/SKILL.md +67 -0
  81. package/skills/mobile-api-network-attacker/SKILL.md +81 -0
  82. package/skills/mobile-binary-hardener/SKILL.md +199 -0
  83. package/skills/mobile-security-specialist/SKILL.md +124 -0
  84. package/skills/mobile-webview-auditor/SKILL.md +200 -0
  85. package/skills/model-extraction-attacker/SKILL.md +68 -0
  86. package/skills/multipart-abuse-tester/SKILL.md +146 -0
  87. package/skills/oauth-pkce-specialist/SKILL.md +191 -0
  88. package/skills/parser-exhaustion-tester/SKILL.md +177 -0
  89. package/skills/pentest-infra/SKILL.md +69 -0
  90. package/skills/pentest-social/SKILL.md +72 -0
  91. package/skills/pentest-team/SKILL.md +126 -0
  92. package/skills/pentest-web-api/SKILL.md +71 -0
  93. package/skills/privacy-flow-analyst/SKILL.md +70 -0
  94. package/skills/prompt-injection-specialist/SKILL.md +76 -0
  95. package/skills/quantum-migration-planner/SKILL.md +184 -0
  96. package/skills/rag-poisoning-specialist/SKILL.md +71 -0
  97. package/skills/registry-mirror-enforcer/SKILL.md +142 -0
  98. package/skills/rotation-validation-agent/SKILL.md +188 -0
  99. package/skills/samm-assessor/SKILL.md +168 -0
  100. package/skills/secrets-mask-bypass-tester/SKILL.md +167 -0
  101. package/skills/senior-security-engineer/SKILL.md +42 -12
  102. package/skills/serialization-memory-attacker/SKILL.md +78 -0
  103. package/skills/session-timeout-tester/SKILL.md +197 -0
  104. package/skills/slsa-level3-enforcer/SKILL.md +185 -0
  105. package/skills/slsa-provenance-enforcer/SKILL.md +181 -0
  106. package/skills/ssrf-detection-validator/SKILL.md +229 -0
  107. package/skills/step-up-auth-enforcer/SKILL.md +176 -0
  108. package/skills/stride-pasta-analyst/SKILL.md +72 -0
  109. package/skills/supply-chain-devsecops/SKILL.md +82 -0
  110. package/skills/threat-infrastructure-analyst/SKILL.md +167 -0
  111. package/skills/threat-modeler/SKILL.md +116 -0
  112. package/skills/tls-certificate-auditor/SKILL.md +76 -0
  113. package/skills/token-reuse-detector/SKILL.md +203 -0
  114. package/skills/trike-risk-modeler/SKILL.md +139 -0
  115. package/skills/unicode-homograph-tester/SKILL.md +179 -0
  116. package/skills/waf-rule-lifecycle-agent/SKILL.md +213 -0
  117. package/skills/webhook-security-tester/SKILL.md +184 -0
  118. package/skills/zero-trust-architect/SKILL.md +211 -0
@@ -0,0 +1,98 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/AbrahamOO/security-mcp/blob/main/defaults/agent-run-schema.json",
4
+ "title": "AgentRunManifest",
5
+ "description": "Schema for .mcp/agent-runs/{agentRunId}/manifest.json — the coordination state for a multi-agent security run.",
6
+ "type": "object",
7
+ "required": ["agentRunId", "runId", "createdAt", "updatedAt", "phase", "internetPermitted", "stackContext", "scope", "agents"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "agentRunId": {
11
+ "type": "string",
12
+ "minLength": 32,
13
+ "maxLength": 32,
14
+ "description": "32-character hex identifier for this agent run."
15
+ },
16
+ "runId": {
17
+ "type": "string",
18
+ "format": "uuid",
19
+ "description": "UUID of the parent review run from security.start_review."
20
+ },
21
+ "createdAt": {
22
+ "type": "string",
23
+ "format": "date-time"
24
+ },
25
+ "updatedAt": {
26
+ "type": "string",
27
+ "format": "date-time"
28
+ },
29
+ "phase": {
30
+ "type": "integer",
31
+ "enum": [0, 1, 2, 3],
32
+ "description": "Current execution phase: 0=init, 1=parallel discovery, 2=adversarial+compliance, 3=synthesis."
33
+ },
34
+ "internetPermitted": {
35
+ "type": "boolean",
36
+ "description": "Whether the user permitted internet access for this run."
37
+ },
38
+ "stackContext": {
39
+ "type": "object",
40
+ "required": ["languages", "frameworks", "databases", "cloudProvider", "paymentProcessor", "hasAI", "hasMobile", "hasPII", "hasPayments", "packageManagers", "ciPlatform"],
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "languages": { "type": "array", "items": { "type": "string" } },
44
+ "frameworks": { "type": "array", "items": { "type": "string" } },
45
+ "databases": { "type": "array", "items": { "type": "string" } },
46
+ "cloudProvider": { "type": "array", "items": { "type": "string" } },
47
+ "paymentProcessor":{ "type": "array", "items": { "type": "string" } },
48
+ "hasAI": { "type": "boolean" },
49
+ "hasMobile": { "type": "boolean" },
50
+ "hasPII": { "type": "boolean" },
51
+ "hasPayments": { "type": "boolean" },
52
+ "packageManagers": { "type": "array", "items": { "type": "string" } },
53
+ "ciPlatform": { "type": "array", "items": { "type": "string" } }
54
+ }
55
+ },
56
+ "scope": {
57
+ "type": "object",
58
+ "required": ["mode", "targets", "baseRef", "headRef"],
59
+ "additionalProperties": false,
60
+ "properties": {
61
+ "mode": {
62
+ "type": "string",
63
+ "enum": ["recent_changes", "folder_by_folder", "file_by_file"]
64
+ },
65
+ "targets": {
66
+ "type": "array",
67
+ "items": { "type": "string" }
68
+ },
69
+ "baseRef": { "type": "string" },
70
+ "headRef": { "type": "string" }
71
+ }
72
+ },
73
+ "agents": {
74
+ "type": "object",
75
+ "description": "Map of agent name to its lifecycle record.",
76
+ "additionalProperties": {
77
+ "$ref": "#/$defs/AgentRecord"
78
+ }
79
+ }
80
+ },
81
+ "$defs": {
82
+ "AgentRecord": {
83
+ "type": "object",
84
+ "required": ["status", "startedAt", "completedAt", "findingsPath", "summary"],
85
+ "additionalProperties": false,
86
+ "properties": {
87
+ "status": {
88
+ "type": "string",
89
+ "enum": ["pending", "running", "completed", "completed_partial", "failed"]
90
+ },
91
+ "startedAt": { "type": ["string", "null"], "format": "date-time" },
92
+ "completedAt": { "type": ["string", "null"], "format": "date-time" },
93
+ "findingsPath": { "type": ["string", "null"] },
94
+ "summary": { "type": ["string", "null"] }
95
+ }
96
+ }
97
+ }
98
+ }
@@ -1,6 +1,8 @@
1
1
  import { runPrGate } from "../gate/policy.js";
2
2
  // Allow safe git revision operators (~ and ^) plus ref/path characters. CWE-88.
3
3
  const SAFE_REF_RE = /^[a-zA-Z0-9_./~^-]+$/;
4
+ // Allow relative file/folder paths for targets. CWE-88.
5
+ const SAFE_TARGET_RE = /^[a-zA-Z0-9_./ -]+$/;
4
6
  function safeEnvRef(envVar, defaultValue) {
5
7
  const val = process.env[envVar] || defaultValue;
6
8
  if (!SAFE_REF_RE.test(val)) {
@@ -9,11 +11,26 @@ function safeEnvRef(envVar, defaultValue) {
9
11
  }
10
12
  return val;
11
13
  }
14
+ function safeEnvTargets(envVar) {
15
+ const raw = process.env[envVar];
16
+ if (!raw)
17
+ return undefined;
18
+ const targets = raw.split(",").map((t) => t.trim()).filter(Boolean);
19
+ return targets.filter((t) => {
20
+ if (!SAFE_TARGET_RE.test(t) || t.includes("..")) {
21
+ console.error(`Skipping unsafe target: "${t}"`);
22
+ return false;
23
+ }
24
+ return true;
25
+ });
26
+ }
12
27
  async function main() {
13
28
  const baseRef = safeEnvRef("SECURITY_GATE_BASE_REF", "origin/main");
14
29
  const headRef = safeEnvRef("SECURITY_GATE_HEAD_REF", "HEAD");
15
30
  const policyPath = process.env.SECURITY_GATE_POLICY || ".mcp/policies/security-policy.json";
16
- const result = await runPrGate({ baseRef, headRef, policyPath });
31
+ const mode = (process.env.SECURITY_GATE_MODE ?? "recent_changes");
32
+ const targets = safeEnvTargets("SECURITY_GATE_TARGETS");
33
+ const result = await runPrGate({ baseRef, headRef, policyPath, mode, targets });
17
34
  // Print result for Actions logs
18
35
  console.log(JSON.stringify(result, null, 2));
19
36
  if (result.status !== "PASS") {
@@ -6,6 +6,7 @@
6
6
  import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
7
7
  import { dirname, join, resolve } from "node:path";
8
8
  import { homedir, platform } from "node:os";
9
+ import * as https from "node:https";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { runOnboarding, installSecurityTools, commandExists, SECURITY_TOOLS } from "./onboarding.js";
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -158,6 +159,71 @@ function installSkill(dryRun) {
158
159
  }
159
160
  process.stdout.write(` ${dryRun ? "[dry-run] would copy" : "installed"} skill: ${skillDest}\n`);
160
161
  }
162
+ /**
163
+ * Download a skill SKILL.md from a remote URL and save it to ~/.claude/skills/{skillName}/SKILL.md.
164
+ * Used for lazy on-demand skill installation — all sub-agents are downloaded this way at first use.
165
+ * Mirrors the same pattern used for security tool binary downloads in onboarding.ts.
166
+ */
167
+ // CWE-22: only alphanumeric, hyphens, and dots allowed in skill names
168
+ const SAFE_SKILL_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
169
+ export async function downloadSkill(skillName, url, dryRun = false) {
170
+ if (!SAFE_SKILL_NAME_RE.test(skillName)) {
171
+ process.stdout.write(` [error] invalid skill name "${skillName}" — skipping download\n`);
172
+ return;
173
+ }
174
+ const skillDest = resolveHome(`~/.claude/skills/${skillName}/SKILL.md`);
175
+ if (dryRun) {
176
+ process.stdout.write(` [dry-run] would download skill "${skillName}" from ${url} → ${skillDest}\n`);
177
+ return;
178
+ }
179
+ const MAX_SKILL_BYTES = 512 * 1024; // 512 KB — skills are markdown files
180
+ const content = await new Promise((resolve) => {
181
+ const req = https.get(url, { headers: { "User-Agent": "security-mcp" } }, (res) => {
182
+ if ((res.statusCode ?? 500) >= 400) {
183
+ res.resume();
184
+ resolve(null);
185
+ return;
186
+ }
187
+ let body = "";
188
+ res.setEncoding("utf8");
189
+ res.on("data", (chunk) => {
190
+ body += chunk;
191
+ if (Buffer.byteLength(body, "utf8") > MAX_SKILL_BYTES) {
192
+ req.destroy();
193
+ resolve(null);
194
+ }
195
+ });
196
+ res.on("end", () => resolve(body));
197
+ });
198
+ req.on("error", () => resolve(null));
199
+ req.setTimeout(10000, () => { req.destroy(); resolve(null); });
200
+ });
201
+ if (!content) {
202
+ process.stdout.write(` [error] failed to download skill "${skillName}" from ${url}\n`);
203
+ return;
204
+ }
205
+ mkdirSync(dirname(skillDest), { recursive: true });
206
+ writeFileSync(skillDest, content, "utf-8");
207
+ process.stdout.write(` installed skill: ${skillDest}\n`);
208
+ }
209
+ /**
210
+ * Eagerly install the orchestrator skill (bundled in the package) plus record
211
+ * its version so orchestration.ensure_skill can detect future updates.
212
+ */
213
+ function installOrchestratorSkill(dryRun) {
214
+ const skillName = "ciso-orchestrator";
215
+ const skillSrc = join(PKG_ROOT, "skills", skillName, "SKILL.md");
216
+ const skillDest = resolveHome(`~/.claude/skills/${skillName}/SKILL.md`);
217
+ if (!existsSync(skillSrc)) {
218
+ process.stdout.write(` [skip] skills/${skillName}/SKILL.md not found in package\n`);
219
+ return;
220
+ }
221
+ if (!dryRun) {
222
+ mkdirSync(dirname(skillDest), { recursive: true });
223
+ copyFileSync(skillSrc, skillDest);
224
+ }
225
+ process.stdout.write(` ${dryRun ? "[dry-run] would copy" : "installed"} skill: ${skillDest}\n`);
226
+ }
161
227
  export async function runInstall(opts) {
162
228
  const dryRun = opts.dryRun;
163
229
  // ── Interactive onboarding (skipped when --yes or non-TTY) ──────────────
@@ -195,11 +261,12 @@ export async function runInstall(opts) {
195
261
  process.stdout.write(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
196
262
  }
197
263
  }
198
- // Install Claude Code skill if Claude Code is in scope
264
+ // Install Claude Code skills if Claude Code is in scope
199
265
  const hasClaudeCode = targets.some((t) => t.name.startsWith("Claude Code"));
200
266
  if (hasClaudeCode || opts.all) {
201
- process.stdout.write("\nInstalling Claude Code skill...\n");
267
+ process.stdout.write("\nInstalling Claude Code skills...\n");
202
268
  installSkill(dryRun);
269
+ installOrchestratorSkill(dryRun);
203
270
  }
204
271
  process.stdout.write("\nInstalling security policy...\n");
205
272
  installPolicy(dryRun);
@@ -12,11 +12,13 @@
12
12
  */
13
13
  import { createInterface } from "node:readline/promises";
14
14
  import { stdin as input, stdout as output } from "node:process";
15
- import { execSync, spawnSync } from "node:child_process";
16
- import { platform, arch, homedir } from "node:os";
17
- import { mkdirSync, createWriteStream, chmodSync, existsSync } from "node:fs";
15
+ import { spawnSync } from "node:child_process";
16
+ import { platform, arch, homedir, tmpdir } from "node:os";
17
+ import { mkdirSync, createWriteStream, chmodSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { pipeline } from "node:stream/promises";
20
+ import { createHash } from "node:crypto";
21
+ import { readFile as readFileAsync } from "node:fs/promises";
20
22
  // ─── Constants ────────────────────────────────────────────────────────────────
21
23
  const PROJECT_TYPES = [
22
24
  {
@@ -204,13 +206,13 @@ function getCpuArch() {
204
206
  }
205
207
  export function commandExists(cmd) {
206
208
  try {
209
+ // Use spawnSync (not execSync) to avoid shell injection — cmd is never interpolated into a shell string
207
210
  if (process.platform === "win32") {
208
- execSync(`where "${cmd}"`, { stdio: "pipe" });
211
+ return spawnSync("where", [cmd], { stdio: "pipe" }).status === 0;
209
212
  }
210
213
  else {
211
- execSync(`command -v ${cmd}`, { stdio: "pipe" });
214
+ return spawnSync("which", [cmd], { stdio: "pipe" }).status === 0;
212
215
  }
213
- return true;
214
216
  }
215
217
  catch {
216
218
  return false;
@@ -220,6 +222,39 @@ function run(cmd, args) {
220
222
  const result = spawnSync(cmd, args, { stdio: "inherit" });
221
223
  return result.status === 0;
222
224
  }
225
+ // ─── Binary integrity helpers ─────────────────────────────────────────────────
226
+ // CWE-494: verify downloaded binary against publisher SHA-256 checksum before install.
227
+ async function fetchChecksumFile(assets) {
228
+ const checksumAsset = assets.find((a) => /checksums?\.txt$/i.test(a.name) || /\.sha256(sums?)?$/i.test(a.name));
229
+ if (!checksumAsset)
230
+ return null;
231
+ try {
232
+ const res = await fetch(checksumAsset.browser_download_url);
233
+ if (!res.ok)
234
+ return null;
235
+ return await res.text();
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ function parseExpectedHash(checksumContent, filename) {
242
+ for (const line of checksumContent.split("\n")) {
243
+ const parts = line.trim().split(/\s+/);
244
+ if (parts.length >= 2) {
245
+ const hash = parts[0];
246
+ const name = (parts.at(-1) ?? "").replace(/^\*/, "");
247
+ if (name === filename && /^[0-9a-f]{64}$/i.test(hash)) {
248
+ return hash.toLowerCase();
249
+ }
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+ async function verifyIntegrity(filePath, expectedHash) {
255
+ const content = await readFileAsync(filePath);
256
+ return createHash("sha256").update(content).digest("hex") === expectedHash;
257
+ }
223
258
  // ─── GitHub binary download ───────────────────────────────────────────────────
224
259
  async function fetchLatestRelease(repo) {
225
260
  try {
@@ -282,6 +317,29 @@ async function installFromGitHub(tool, os) {
282
317
  print(` Download failed.`);
283
318
  return false;
284
319
  }
320
+ // CWE-494: verify SHA-256 integrity before executing anything
321
+ const checksumContent = await fetchChecksumFile(release.assets);
322
+ if (checksumContent) {
323
+ const expectedHash = parseExpectedHash(checksumContent, fileName);
324
+ if (expectedHash) {
325
+ const valid = await verifyIntegrity(tmpFile, expectedHash);
326
+ if (!valid) {
327
+ print(` Integrity check FAILED for ${fileName} — aborting install.`);
328
+ try {
329
+ unlinkSync(tmpFile);
330
+ }
331
+ catch { /* ignore cleanup failure */ }
332
+ return false;
333
+ }
334
+ print(` Integrity verified (SHA-256 matched).`);
335
+ }
336
+ else {
337
+ print(` Warning: checksum file found but no entry for ${fileName} — proceeding without verification.`);
338
+ }
339
+ }
340
+ else {
341
+ print(` Warning: no checksum file in release assets — cannot verify binary integrity.`);
342
+ }
285
343
  const destDir = "/usr/local/bin";
286
344
  if (tool.tarball) {
287
345
  // Extract the binary from the archive
@@ -347,12 +405,24 @@ async function tryDnf(tool) {
347
405
  const mgr = commandExists("dnf") ? "dnf" : commandExists("yum") ? "yum" : null;
348
406
  if (!mgr)
349
407
  return false;
350
- // Add Aqua Security rpm repo
351
- const repoContent = "[trivy]\\nname=Trivy repository\\n" +
352
- "baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/\\n" +
353
- "gpgcheck=0\\nenabled=1";
408
+ // CWE-78: avoid bash -c shell construction — write repo file to a temp path
409
+ // then move it into place with sudo (no shell, no injection surface).
410
+ const repoLines = [
411
+ "[trivy]",
412
+ "name=Trivy repository",
413
+ "baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/",
414
+ "gpgcheck=0",
415
+ "enabled=1"
416
+ ];
417
+ const tmpRepoFile = join(tmpdir(), `trivy-${Date.now()}.repo`);
354
418
  print(` Adding Aqua Security yum/dnf repository...`);
355
- run("bash", ["-c", `echo -e "${repoContent}" | sudo tee /etc/yum.repos.d/trivy.repo`]);
419
+ try {
420
+ writeFileSync(tmpRepoFile, repoLines.join("\n") + "\n", "utf-8");
421
+ }
422
+ catch {
423
+ return false;
424
+ }
425
+ run("sudo", ["mv", tmpRepoFile, "/etc/yum.repos.d/trivy.repo"]);
356
426
  print(` sudo ${mgr} install -y trivy`);
357
427
  return run("sudo", [mgr, "install", "-y", "trivy"]);
358
428
  }
@@ -513,6 +583,7 @@ export async function runOnboarding() {
513
583
  print("");
514
584
  print(" This applies the right compliance controls automatically,");
515
585
  print(" such as PCI DSS for payment cards or HIPAA for health data.");
586
+ print(" You can select multiple options (e.g. 1 2 or 1,2).");
516
587
  print("");
517
588
  for (const d of SENSITIVE_DATA_OPTIONS) {
518
589
  print(` ${d.key}. ${d.label}`);
@@ -4,11 +4,13 @@ import { dirname, join } from "node:path";
4
4
  import * as https from "node:https";
5
5
  const CACHE_DIR = join(homedir(), ".security-mcp");
6
6
  const CACHE_PATH = join(CACHE_DIR, "update-check.json");
7
+ const SKILL_VERSIONS_PATH = join(CACHE_DIR, "skill-versions.json");
7
8
  const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
8
9
  const PROMPT_INTERVAL_MS = 24 * 60 * 60 * 1000;
9
10
  const REGISTRY_URL = "https://registry.npmjs.org/security-mcp/latest";
11
+ const SKILLS_MANIFEST_URL = "https://raw.githubusercontent.com/AbrahamOO/security-mcp/main/skills-manifest.json";
10
12
  function parseVersion(input) {
11
- const match = input.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
13
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(input.trim());
12
14
  if (!match)
13
15
  return null;
14
16
  return {
@@ -64,10 +66,15 @@ function fetchLatestVersion(timeoutMs = 1500) {
64
66
  resolve(null);
65
67
  return;
66
68
  }
69
+ const MAX_BYTES = 64 * 1024; // 64 KB — npm registry version response
67
70
  let body = "";
68
71
  res.setEncoding("utf8");
69
72
  res.on("data", (chunk) => {
70
73
  body += chunk;
74
+ if (Buffer.byteLength(body, "utf8") > MAX_BYTES) {
75
+ req.destroy();
76
+ resolve(null);
77
+ }
71
78
  });
72
79
  res.on("end", () => {
73
80
  try {
@@ -96,6 +103,71 @@ function shouldPrompt(cache, latestVersion, now) {
96
103
  return true;
97
104
  return now - lastPromptedAt >= PROMPT_INTERVAL_MS;
98
105
  }
106
+ /** Check the skills manifest for skills that have newer versions than what is locally installed. */
107
+ async function checkSkillUpdates() {
108
+ try {
109
+ const body = await new Promise((resolve) => {
110
+ const req = https.get(SKILLS_MANIFEST_URL, { headers: { "User-Agent": "security-mcp-update-checker" } }, (res) => {
111
+ if ((res.statusCode ?? 500) >= 400) {
112
+ res.resume();
113
+ resolve(null);
114
+ return;
115
+ }
116
+ const MAX_MANIFEST_BYTES = 256 * 1024; // 256 KB
117
+ let buf = "";
118
+ res.setEncoding("utf8");
119
+ res.on("data", (c) => {
120
+ buf += c;
121
+ if (Buffer.byteLength(buf, "utf8") > MAX_MANIFEST_BYTES) {
122
+ req.destroy();
123
+ resolve(null);
124
+ }
125
+ });
126
+ res.on("end", () => resolve(buf));
127
+ });
128
+ req.on("error", () => resolve(null));
129
+ req.setTimeout(3000, () => { req.destroy(); resolve(null); });
130
+ });
131
+ if (!body)
132
+ return [];
133
+ const manifest = JSON.parse(body);
134
+ let installed = {};
135
+ try {
136
+ installed = JSON.parse(readFileSync(SKILL_VERSIONS_PATH, "utf-8"));
137
+ }
138
+ catch { /* not installed yet */ }
139
+ const outdated = [];
140
+ for (const [name, entry] of Object.entries(manifest.skills)) {
141
+ const local = installed[name]?.version;
142
+ if (local && local !== entry.version) {
143
+ outdated.push(`${name}: ${local} → ${entry.version}`);
144
+ }
145
+ }
146
+ return outdated;
147
+ }
148
+ catch {
149
+ return [];
150
+ }
151
+ }
152
+ function printUpdateNotices(cache, currentVersion, now) {
153
+ const hasMcpUpdate = cache.latestVersion && compareVersions(currentVersion, cache.latestVersion) < 0;
154
+ const hasSkillUpdates = (cache.skillsWithUpdates?.length ?? 0) > 0;
155
+ if (!hasMcpUpdate && !hasSkillUpdates)
156
+ return;
157
+ if (cache.latestVersion && !shouldPrompt(cache, cache.latestVersion, now))
158
+ return;
159
+ if (hasMcpUpdate && cache.latestVersion) {
160
+ console.error(`\nUpdate available: security-mcp ${currentVersion} → ${cache.latestVersion}\n` +
161
+ "Run the CISO Orchestrator skill and choose option (A) to update automatically, or:\n" +
162
+ ` npm install -g security-mcp@${cache.latestVersion}\n` +
163
+ " security-mcp install\n");
164
+ }
165
+ if (hasSkillUpdates && cache.skillsWithUpdates) {
166
+ console.error("\nSkill updates available:\n" +
167
+ cache.skillsWithUpdates.map((s) => ` • ${s}`).join("\n") +
168
+ "\nRun the CISO Orchestrator skill to apply skill updates automatically.\n");
169
+ }
170
+ }
99
171
  export async function notifyIfUpdateAvailable(currentVersion) {
100
172
  const now = Date.now();
101
173
  const cache = readCache();
@@ -103,22 +175,18 @@ export async function notifyIfUpdateAvailable(currentVersion) {
103
175
  const shouldRefresh = Number.isNaN(lastCheckedAt) || now - lastCheckedAt >= CHECK_INTERVAL_MS;
104
176
  if (shouldRefresh) {
105
177
  const latestVersion = await fetchLatestVersion();
106
- if (latestVersion) {
178
+ if (latestVersion)
107
179
  cache.latestVersion = latestVersion;
108
- }
180
+ const skillUpdates = await checkSkillUpdates();
181
+ if (skillUpdates.length > 0)
182
+ cache.skillsWithUpdates = skillUpdates;
109
183
  cache.lastCheckedAt = new Date(now).toISOString();
110
184
  writeCache(cache);
111
185
  }
112
- if (!cache.latestVersion)
113
- return;
114
- if (compareVersions(currentVersion, cache.latestVersion) >= 0)
115
- return;
116
- if (!shouldPrompt(cache, cache.latestVersion, now))
117
- return;
118
- process.stderr.write(`\nUpdate available: security-mcp ${currentVersion} -> ${cache.latestVersion}\n` +
119
- "Update command: npm install -g security-mcp@latest\n" +
120
- "Then refresh editor config: security-mcp install-global\n\n");
121
- cache.lastPromptedVersion = cache.latestVersion;
122
- cache.lastPromptedAt = new Date(now).toISOString();
123
- writeCache(cache);
186
+ printUpdateNotices(cache, currentVersion, now);
187
+ if (cache.latestVersion) {
188
+ cache.lastPromptedVersion = cache.latestVersion;
189
+ cache.lastPromptedAt = new Date(now).toISOString();
190
+ writeCache(cache);
191
+ }
124
192
  }
@@ -294,16 +294,91 @@ async function runDynamicProbes(endpointUrl, probes) {
294
294
  }
295
295
  return results;
296
296
  }
297
+ // CWE-918: resolve and validate an endpoint URL is not pointing to a private/metadata host.
298
+ const PROBE_PRIVATE_RE = [/^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^169\.254\./, /^::1$/, /^fc/, /^fd/];
299
+ function isProbePrivateIp(ip) {
300
+ return PROBE_PRIVATE_RE.some((r) => r.test(ip));
301
+ }
302
+ async function resolveSafeEndpoint(rawUrl) {
303
+ try {
304
+ const parsed = new URL(rawUrl);
305
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:")
306
+ return null;
307
+ const { lookup } = await import("node:dns/promises");
308
+ const { isIP } = await import("node:net");
309
+ const host = parsed.hostname;
310
+ if (isIP(host))
311
+ return isProbePrivateIp(host) ? null : rawUrl;
312
+ if (host === "localhost" || host.endsWith(".internal"))
313
+ return null;
314
+ const resolved = await lookup(host, { all: true });
315
+ if (resolved.some(({ address }) => isProbePrivateIp(address)))
316
+ return null;
317
+ return rawUrl;
318
+ }
319
+ catch {
320
+ return null;
321
+ }
322
+ }
323
+ function probeFailureToFinding(probe) {
324
+ switch (probe.probe) {
325
+ case "jailbreak": return {
326
+ id: "AI_JAILBREAK_SUCCESS",
327
+ title: "Jailbreak probe succeeded — system prompt may have leaked",
328
+ severity: "CRITICAL",
329
+ evidence: ["Probe: jailbreak", probe.detail ?? ""],
330
+ requiredActions: [
331
+ "Implement system prompt protection: use instruction hierarchy, not string concatenation.",
332
+ "Add jailbreak detection and monitoring.",
333
+ "Do not rely on the system prompt for access control."
334
+ ]
335
+ };
336
+ case "injection": return {
337
+ id: "AI_INJECTION_SUCCESS",
338
+ title: "Prompt injection probe succeeded via simulated RAG context",
339
+ severity: "CRITICAL",
340
+ evidence: ["Probe: injection"],
341
+ requiredActions: [
342
+ "Apply content isolation between user instructions and retrieved context.",
343
+ "Treat all RAG-retrieved content as untrusted.",
344
+ "Validate model outputs before acting on them."
345
+ ]
346
+ };
347
+ case "pii_exfil": return {
348
+ id: "AI_PII_LEAK",
349
+ title: "PII exfiltration probe succeeded — model repeated sensitive data",
350
+ severity: "CRITICAL",
351
+ evidence: ["Probe: pii_exfil"],
352
+ requiredActions: [
353
+ "Implement output PII scanning before returning model responses.",
354
+ "Block responses containing SSN, card numbers, or credential patterns.",
355
+ "Add output filtering as a defense-in-depth layer."
356
+ ]
357
+ };
358
+ case "token_flood": return {
359
+ id: "AI_RATE_LIMIT_MISSING",
360
+ title: "Token flooding probe was not rate-limited — DoS risk",
361
+ severity: "HIGH",
362
+ evidence: ["Probe: token_flood"],
363
+ requiredActions: [
364
+ "Implement request size limits and token quotas on AI endpoints.",
365
+ "Return 413 or 429 for oversized requests.",
366
+ "Add per-user token budgets."
367
+ ]
368
+ };
369
+ default: return null;
370
+ }
371
+ }
297
372
  /**
298
373
  * Run AI/LLM red-team checks: static analysis + optional dynamic probes.
299
374
  */
300
375
  export async function runAiRedteamChecks(opts) {
301
376
  const findings = [];
302
- // Static analysis (always runs)
303
- const staticFindings = await runStaticAnalysis(opts.changedFiles);
304
- findings.push(...staticFindings);
305
- // Dynamic probes (only if endpoint is configured)
306
- const endpointUrl = opts.endpointUrl ?? process.env["SECURITY_AI_ENDPOINT"];
377
+ findings.push(...await runStaticAnalysis(opts.changedFiles));
378
+ const rawEndpointUrl = opts.endpointUrl ?? process.env["SECURITY_AI_ENDPOINT"];
379
+ if (!rawEndpointUrl)
380
+ return findings;
381
+ const endpointUrl = await resolveSafeEndpoint(rawEndpointUrl);
307
382
  if (!endpointUrl)
308
383
  return findings;
309
384
  const allProbes = ["jailbreak", "injection", "pii_exfil", "token_flood"];
@@ -314,60 +389,9 @@ export async function runAiRedteamChecks(opts) {
314
389
  for (const probe of result.value) {
315
390
  if (probe.passed)
316
391
  continue;
317
- switch (probe.probe) {
318
- case "jailbreak":
319
- findings.push({
320
- id: "AI_JAILBREAK_SUCCESS",
321
- title: "Jailbreak probe succeeded — system prompt may have leaked",
322
- severity: "CRITICAL",
323
- evidence: ["Probe: jailbreak", probe.detail ?? ""],
324
- requiredActions: [
325
- "Implement system prompt protection: use instruction hierarchy, not string concatenation.",
326
- "Add jailbreak detection and monitoring.",
327
- "Do not rely on the system prompt for access control."
328
- ]
329
- });
330
- break;
331
- case "injection":
332
- findings.push({
333
- id: "AI_INJECTION_SUCCESS",
334
- title: "Prompt injection probe succeeded via simulated RAG context",
335
- severity: "CRITICAL",
336
- evidence: ["Probe: injection"],
337
- requiredActions: [
338
- "Apply content isolation between user instructions and retrieved context.",
339
- "Treat all RAG-retrieved content as untrusted.",
340
- "Validate model outputs before acting on them."
341
- ]
342
- });
343
- break;
344
- case "pii_exfil":
345
- findings.push({
346
- id: "AI_PII_LEAK",
347
- title: "PII exfiltration probe succeeded — model repeated sensitive data",
348
- severity: "CRITICAL",
349
- evidence: ["Probe: pii_exfil"],
350
- requiredActions: [
351
- "Implement output PII scanning before returning model responses.",
352
- "Block responses containing SSN, card numbers, or credential patterns.",
353
- "Add output filtering as a defense-in-depth layer."
354
- ]
355
- });
356
- break;
357
- case "token_flood":
358
- findings.push({
359
- id: "AI_RATE_LIMIT_MISSING",
360
- title: "Token flooding probe was not rate-limited — DoS risk",
361
- severity: "HIGH",
362
- evidence: ["Probe: token_flood"],
363
- requiredActions: [
364
- "Implement request size limits and token quotas on AI endpoints.",
365
- "Return 413 or 429 for oversized requests.",
366
- "Add per-user token budgets."
367
- ]
368
- });
369
- break;
370
- }
392
+ const finding = probeFailureToFinding(probe);
393
+ if (finding)
394
+ findings.push(finding);
371
395
  }
372
396
  }
373
397
  return findings;