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.
- package/README.md +966 -193
- package/defaults/agent-run-schema.json +98 -0
- package/dist/ci/pr-gate.js +18 -1
- package/dist/cli/install.js +69 -2
- package/dist/cli/onboarding.js +82 -11
- package/dist/cli/update.js +83 -15
- package/dist/gate/checks/ai-redteam.js +83 -59
- package/dist/gate/checks/api.js +93 -0
- package/dist/gate/checks/ci-pipeline.js +135 -0
- package/dist/gate/checks/crypto.js +91 -22
- package/dist/gate/checks/database.js +5 -1
- package/dist/gate/checks/dependencies.js +297 -2
- package/dist/gate/checks/dlp.js +6 -1
- package/dist/gate/checks/graphql.js +6 -1
- package/dist/gate/checks/k8s.js +229 -181
- package/dist/gate/checks/nuclei.js +133 -0
- package/dist/gate/checks/runtime.js +75 -8
- package/dist/gate/checks/scanners.js +8 -2
- package/dist/gate/diff.js +2 -0
- package/dist/gate/exceptions.js +6 -1
- package/dist/gate/policy.js +47 -4
- package/dist/gate/result.js +7 -1
- package/dist/mcp/audit-chain.js +253 -0
- package/dist/mcp/learning.js +228 -0
- package/dist/mcp/model-router.js +544 -0
- package/dist/mcp/orchestration.js +604 -0
- package/dist/mcp/server.js +160 -12
- package/dist/repo/search.js +5 -7
- package/dist/review/store.js +15 -0
- package/dist/types/agent-run.js +8 -0
- package/package.json +5 -5
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +225 -0
- package/skills/agentic-loop-exploiter/SKILL.md +69 -0
- package/skills/ai-llm-redteam/SKILL.md +118 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +198 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
- package/skills/android-penetration-tester/SKILL.md +83 -0
- package/skills/anti-replay-tester/SKILL.md +195 -0
- package/skills/appsec-code-auditor/SKILL.md +86 -0
- package/skills/artifact-integrity-analyst/SKILL.md +68 -0
- package/skills/attack-navigator/SKILL.md +64 -0
- package/skills/auth-session-hacker/SKILL.md +87 -0
- package/skills/aws-penetration-tester/SKILL.md +60 -0
- package/skills/azure-penetration-tester/SKILL.md +64 -0
- package/skills/binary-auth-validator/SKILL.md +184 -0
- package/skills/bot-detection-specialist/SKILL.md +221 -0
- package/skills/business-logic-attacker/SKILL.md +76 -0
- package/skills/capec-code-mapper/SKILL.md +163 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +200 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
- package/skills/ciso-orchestrator/SKILL.md +165 -0
- package/skills/cloud-infra-specialist/SKILL.md +85 -0
- package/skills/compliance-gap-analyst/SKILL.md +77 -0
- package/skills/compliance-grc/SKILL.md +148 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +169 -0
- package/skills/credential-stuffing-specialist/SKILL.md +192 -0
- package/skills/crypto-pki-specialist/SKILL.md +136 -0
- package/skills/csa-ccm-mapper/SKILL.md +178 -0
- package/skills/csf2-governance-mapper/SKILL.md +159 -0
- package/skills/deep-link-fuzzer/SKILL.md +195 -0
- package/skills/dependency-confusion-attacker/SKILL.md +78 -0
- package/skills/device-integrity-aggregator/SKILL.md +221 -0
- package/skills/dos-resilience-tester/SKILL.md +184 -0
- package/skills/dread-scorer/SKILL.md +157 -0
- package/skills/egress-policy-enforcer/SKILL.md +208 -0
- package/skills/evidence-collector/SKILL.md +86 -0
- package/skills/file-upload-attacker/SKILL.md +208 -0
- package/skills/gcp-penetration-tester/SKILL.md +63 -0
- package/skills/git-history-secret-scanner/SKILL.md +182 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +216 -0
- package/skills/incident-responder/SKILL.md +192 -0
- package/skills/injection-specialist/SKILL.md +62 -0
- package/skills/ios-security-auditor/SKILL.md +77 -0
- package/skills/json-ambiguity-tester/SKILL.md +175 -0
- package/skills/k8s-container-escaper/SKILL.md +74 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
- package/skills/kill-switch-engineer/SKILL.md +205 -0
- package/skills/linddun-privacy-analyst/SKILL.md +196 -0
- package/skills/logic-race-fuzzer/SKILL.md +67 -0
- package/skills/mobile-api-network-attacker/SKILL.md +81 -0
- package/skills/mobile-binary-hardener/SKILL.md +199 -0
- package/skills/mobile-security-specialist/SKILL.md +124 -0
- package/skills/mobile-webview-auditor/SKILL.md +200 -0
- package/skills/model-extraction-attacker/SKILL.md +68 -0
- package/skills/multipart-abuse-tester/SKILL.md +146 -0
- package/skills/oauth-pkce-specialist/SKILL.md +191 -0
- package/skills/parser-exhaustion-tester/SKILL.md +177 -0
- package/skills/pentest-infra/SKILL.md +69 -0
- package/skills/pentest-social/SKILL.md +72 -0
- package/skills/pentest-team/SKILL.md +126 -0
- package/skills/pentest-web-api/SKILL.md +71 -0
- package/skills/privacy-flow-analyst/SKILL.md +70 -0
- package/skills/prompt-injection-specialist/SKILL.md +76 -0
- package/skills/quantum-migration-planner/SKILL.md +184 -0
- package/skills/rag-poisoning-specialist/SKILL.md +71 -0
- package/skills/registry-mirror-enforcer/SKILL.md +142 -0
- package/skills/rotation-validation-agent/SKILL.md +188 -0
- package/skills/samm-assessor/SKILL.md +168 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +167 -0
- package/skills/senior-security-engineer/SKILL.md +42 -12
- package/skills/serialization-memory-attacker/SKILL.md +78 -0
- package/skills/session-timeout-tester/SKILL.md +197 -0
- package/skills/slsa-level3-enforcer/SKILL.md +185 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +181 -0
- package/skills/ssrf-detection-validator/SKILL.md +229 -0
- package/skills/step-up-auth-enforcer/SKILL.md +176 -0
- package/skills/stride-pasta-analyst/SKILL.md +72 -0
- package/skills/supply-chain-devsecops/SKILL.md +82 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +167 -0
- package/skills/threat-modeler/SKILL.md +116 -0
- package/skills/tls-certificate-auditor/SKILL.md +76 -0
- package/skills/token-reuse-detector/SKILL.md +203 -0
- package/skills/trike-risk-modeler/SKILL.md +139 -0
- package/skills/unicode-homograph-tester/SKILL.md +179 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +213 -0
- package/skills/webhook-security-tester/SKILL.md +184 -0
- 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
|
+
}
|
package/dist/ci/pr-gate.js
CHANGED
|
@@ -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
|
|
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") {
|
package/dist/cli/install.js
CHANGED
|
@@ -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
|
|
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
|
|
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);
|
package/dist/cli/onboarding.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
211
|
+
return spawnSync("where", [cmd], { stdio: "pipe" }).status === 0;
|
|
209
212
|
}
|
|
210
213
|
else {
|
|
211
|
-
|
|
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
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
"
|
|
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
|
-
|
|
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}`);
|
package/dist/cli/update.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const endpointUrl =
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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;
|