security-mcp 1.0.5 → 1.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.
Files changed (81) hide show
  1. package/README.md +963 -193
  2. package/defaults/agent-run-schema.json +98 -0
  3. package/defaults/checklists/ai.json +25 -0
  4. package/defaults/checklists/api.json +27 -0
  5. package/defaults/checklists/infra.json +27 -0
  6. package/defaults/checklists/mobile.json +25 -0
  7. package/defaults/checklists/payments.json +25 -0
  8. package/defaults/checklists/web.json +30 -0
  9. package/defaults/control-catalog.json +392 -0
  10. package/defaults/evidence-map.json +194 -0
  11. package/defaults/security-policy.json +41 -2
  12. package/dist/cli/index.js +13 -8
  13. package/dist/cli/install.js +80 -2
  14. package/dist/cli/onboarding.js +590 -0
  15. package/dist/cli/update.js +83 -15
  16. package/dist/gate/baseline.js +115 -0
  17. package/dist/gate/checks/ai-redteam.js +398 -0
  18. package/dist/gate/checks/api.js +93 -0
  19. package/dist/gate/checks/crypto.js +153 -0
  20. package/dist/gate/checks/database.js +144 -0
  21. package/dist/gate/checks/dependencies.js +126 -0
  22. package/dist/gate/checks/dlp.js +153 -0
  23. package/dist/gate/checks/graphql.js +122 -0
  24. package/dist/gate/checks/infra.js +126 -12
  25. package/dist/gate/checks/k8s.js +190 -0
  26. package/dist/gate/checks/playbook.js +160 -0
  27. package/dist/gate/checks/runtime.js +316 -0
  28. package/dist/gate/checks/sbom.js +199 -0
  29. package/dist/gate/checks/scanners.js +379 -8
  30. package/dist/gate/checks/secrets.js +85 -20
  31. package/dist/gate/exceptions.js +6 -1
  32. package/dist/gate/policy.js +85 -19
  33. package/dist/gate/threat-intel.js +157 -0
  34. package/dist/mcp/orchestration.js +586 -0
  35. package/dist/mcp/server.js +568 -16
  36. package/dist/repo/search.js +11 -1
  37. package/dist/review/store.js +133 -0
  38. package/dist/types/agent-run.js +8 -0
  39. package/package.json +5 -5
  40. package/prompts/SECURITY_PROMPT.md +415 -1
  41. package/skills/agentic-loop-exploiter/SKILL.md +69 -0
  42. package/skills/ai-llm-redteam/SKILL.md +118 -0
  43. package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
  44. package/skills/android-penetration-tester/SKILL.md +83 -0
  45. package/skills/appsec-code-auditor/SKILL.md +86 -0
  46. package/skills/artifact-integrity-analyst/SKILL.md +68 -0
  47. package/skills/attack-navigator/SKILL.md +64 -0
  48. package/skills/auth-session-hacker/SKILL.md +87 -0
  49. package/skills/aws-penetration-tester/SKILL.md +60 -0
  50. package/skills/azure-penetration-tester/SKILL.md +64 -0
  51. package/skills/business-logic-attacker/SKILL.md +76 -0
  52. package/skills/cicd-pipeline-hijacker/SKILL.md +81 -0
  53. package/skills/ciso-orchestrator/SKILL.md +165 -0
  54. package/skills/cloud-infra-specialist/SKILL.md +85 -0
  55. package/skills/compliance-gap-analyst/SKILL.md +77 -0
  56. package/skills/compliance-grc/SKILL.md +148 -0
  57. package/skills/crypto-pki-specialist/SKILL.md +136 -0
  58. package/skills/dependency-confusion-attacker/SKILL.md +78 -0
  59. package/skills/evidence-collector/SKILL.md +86 -0
  60. package/skills/gcp-penetration-tester/SKILL.md +63 -0
  61. package/skills/injection-specialist/SKILL.md +62 -0
  62. package/skills/ios-security-auditor/SKILL.md +77 -0
  63. package/skills/k8s-container-escaper/SKILL.md +74 -0
  64. package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
  65. package/skills/logic-race-fuzzer/SKILL.md +67 -0
  66. package/skills/mobile-api-network-attacker/SKILL.md +81 -0
  67. package/skills/mobile-security-specialist/SKILL.md +124 -0
  68. package/skills/model-extraction-attacker/SKILL.md +68 -0
  69. package/skills/pentest-infra/SKILL.md +69 -0
  70. package/skills/pentest-social/SKILL.md +72 -0
  71. package/skills/pentest-team/SKILL.md +126 -0
  72. package/skills/pentest-web-api/SKILL.md +71 -0
  73. package/skills/privacy-flow-analyst/SKILL.md +70 -0
  74. package/skills/prompt-injection-specialist/SKILL.md +76 -0
  75. package/skills/rag-poisoning-specialist/SKILL.md +71 -0
  76. package/skills/senior-security-engineer/SKILL.md +75 -13
  77. package/skills/serialization-memory-attacker/SKILL.md +78 -0
  78. package/skills/stride-pasta-analyst/SKILL.md +72 -0
  79. package/skills/supply-chain-devsecops/SKILL.md +82 -0
  80. package/skills/threat-modeler/SKILL.md +116 -0
  81. package/skills/tls-certificate-auditor/SKILL.md +76 -0
@@ -8,19 +8,23 @@ import { runPrGate } from "../gate/policy.js";
8
8
  import { readFileSafe } from "../repo/fs.js";
9
9
  import { searchRepo } from "../repo/search.js";
10
10
  import { createReviewAttestation, createReviewRun, readReviewRun, updateReviewStep } from "../review/store.js";
11
+ import { createAgentRun, CreateAgentRunSchema, updateAgentStatus, UpdateAgentStatusSchema, mergeAgentFindings, MergeAgentFindingsSchema, ensureSkill, EnsureSkillSchema, readAgentMemory, ReadAgentMemorySchema, writeAgentMemory, WriteAgentMemorySchema, checkUpdates, CheckUpdatesSchema, applyUpdates, ApplyUpdatesSchema, verifySkillCoverage, VerifySkillCoverageSchema } from "./orchestration.js";
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
13
  const PKG_ROOT = resolve(__dirname, "../..");
13
14
  const PROMPTS_DIR = join(PKG_ROOT, "prompts");
14
- // Load the generalized security prompt at startup.
15
- // Falls back to a short notice if the file has not been built yet.
16
- function loadPromptFile(name) {
17
- const path = join(PROMPTS_DIR, name);
18
- if (existsSync(path)) {
19
- return readFileSync(path, "utf-8");
20
- }
21
- return `[security-mcp] Prompt file not found: ${name}. Run "npm run build" from the package root.`;
15
+ // Lazily load the security prompt on first use rather than at server startup.
16
+ // This avoids injecting ~19K tokens into every session that doesn't call a
17
+ // security tool (e.g. non-security MCP usage in the same editor).
18
+ let _securityPromptCache = null;
19
+ function getSecurityPrompt() {
20
+ if (_securityPromptCache !== null)
21
+ return _securityPromptCache;
22
+ const path = join(PROMPTS_DIR, "SECURITY_PROMPT.md");
23
+ _securityPromptCache = existsSync(path)
24
+ ? readFileSync(path, "utf-8")
25
+ : `[security-mcp] Prompt file not found. Run "npm run build" from the package root.`;
26
+ return _securityPromptCache;
22
27
  }
23
- const SECURITY_PROMPT = loadPromptFile("SECURITY_PROMPT.md");
24
28
  const server = new McpServer({
25
29
  name: "security-mcp",
26
30
  version: "1.0.0"
@@ -62,7 +66,7 @@ const StartReviewParams = {
62
66
  headRef: z.string().optional().describe("Only for recent_changes mode. Head git ref, default HEAD.")
63
67
  };
64
68
  const StartReviewSchema = z.object(StartReviewParams);
65
- tool("security.start_review", "Start a stateful security review run, lock the scan mode, and return a run ID for ordered execution and attestation.", StartReviewParams, safeTool(async (args, _extra) => {
69
+ tool("security.start_review", "Start a stateful security review run, lock the scan mode, and return a run ID for ordered execution and attestation. OPERATING MANDATE: 90% fixing, 10% advisory. You do not list vulnerabilities and walk away — you write the fix, implement the control, and enforce the policy.", StartReviewParams, safeTool(async (args, _extra) => {
66
70
  const { mode, targets, baseRef, headRef } = StartReviewSchema.parse(args);
67
71
  const cleanTargets = (targets ?? []).map((target) => target.trim()).filter(Boolean);
68
72
  if ((mode === "folder_by_folder" || mode === "file_by_file") && cleanTargets.length === 0) {
@@ -82,6 +86,7 @@ tool("security.start_review", "Start a stateful security review run, lock the sc
82
86
  baseRef: baseRef ?? "origin/main",
83
87
  headRef: headRef ?? "HEAD",
84
88
  requiredSteps: run.requiredSteps,
89
+ operatingMandate: "90% fixing, 10% advisory. Write the fix. Implement the control. Enforce the policy. Do not list vulnerabilities and walk away.",
85
90
  nextSteps: [
86
91
  "Run security.threat_model with this runId.",
87
92
  "Run security.checklist with this runId.",
@@ -90,9 +95,14 @@ tool("security.start_review", "Start a stateful security review run, lock the sc
90
95
  ]
91
96
  });
92
97
  }));
98
+ // CWE-200: restrict to SECURITY_-prefixed names so callers cannot probe arbitrary env vars
99
+ const ATTEST_ENV_VAR_RE = /^SECURITY_[A-Z][A-Z0-9_]{0,63}$/;
93
100
  const AttestReviewParams = {
94
101
  runId: z.string().uuid().describe("Security review run ID."),
95
- signatureEnvVar: z.string().optional().describe("Optional environment variable containing an HMAC key for attestation signing.")
102
+ signatureEnvVar: z.string()
103
+ .regex(ATTEST_ENV_VAR_RE, "signatureEnvVar must be a SECURITY_-prefixed env var name (e.g. SECURITY_ATTEST_KEY)")
104
+ .optional()
105
+ .describe("Optional SECURITY_-prefixed environment variable containing an HMAC key for attestation signing.")
96
106
  };
97
107
  const AttestReviewSchema = z.object(AttestReviewParams);
98
108
  tool("security.attest_review", "Generate a security review attestation with integrity hash and optional HMAC signature.", AttestReviewParams, safeTool(async (args, _extra) => {
@@ -199,9 +209,20 @@ const GetSystemPromptParams = {
199
209
  payment_processor: z.string().optional().describe("Payment processor in use, e.g. 'Stripe', 'Braintree', 'Adyen', or 'none'.")
200
210
  };
201
211
  const GetSystemPromptSchema = z.object(GetSystemPromptParams);
202
- tool("security.get_system_prompt", "Return the full security engineering system prompt. Optionally customized with your stack, cloud provider, and payment processor. Use this as the system prompt to configure Claude as an elite security engineer for your project.", GetSystemPromptParams, safeTool(async (args, _extra) => {
212
+ tool("security.get_system_prompt", "Return the full security engineering system prompt. Optionally customized with your stack, cloud provider, and payment processor. Use this as the system prompt to configure Claude as an elite security engineer for your project. Core operating ratio: 90% fixing, 10% advisory — write the fix, implement the control, enforce the policy.", GetSystemPromptParams, safeTool(async (args, _extra) => {
203
213
  const { stack, cloud, payment_processor } = GetSystemPromptSchema.parse(args);
204
- let prompt = SECURITY_PROMPT;
214
+ // Prepend the operating mandate so it is the first instruction the model reads,
215
+ // regardless of which part of the prompt file is loaded or truncated.
216
+ const OPERATING_MANDATE = "# CORE OPERATING MANDATE — READ FIRST\n\n" +
217
+ "Your operating ratio is **90% fixing, 10% advisory**.\n" +
218
+ "You do NOT list vulnerabilities and walk away.\n" +
219
+ "You write the fix. You implement the control. You enforce the policy.\n\n" +
220
+ "**90% action:** Write the secure code directly. Implement validation, middleware, " +
221
+ "access controls, and secret management. Produce production-ready fixes every time.\n\n" +
222
+ "**10% explanation:** One line — what was wrong, what attack it prevents, which framework " +
223
+ "control applies (OWASP, ATT&CK, NIST). Then move on.\n\n" +
224
+ "---\n\n";
225
+ let prompt = OPERATING_MANDATE + getSecurityPrompt();
205
226
  // Append a project-specific scope section if any context was provided
206
227
  if (stack ?? cloud ?? payment_processor) {
207
228
  const scopeLines = [
@@ -808,6 +829,85 @@ deny contains msg if {
808
829
  }
809
830
  };
810
831
  const selected = policyByPack[selectedPack];
832
+ // Generate test file for the selected policy pack
833
+ const testPackageName = `security.${selectedPack.replace(/_/g, "")}_test`;
834
+ const testPolicy = `package ${testPackageName}
835
+
836
+ import rego.v1
837
+
838
+ # --- Allow cases (should NOT produce deny) ---
839
+
840
+ test_allow_valid_resource if {
841
+ count(deny) == 0 with input as {
842
+ "resource_changes": []
843
+ }
844
+ }
845
+
846
+ test_allow_encrypted_storage if {
847
+ count(deny) == 0 with input as {
848
+ "resource_changes": [{
849
+ "type": "aws_s3_bucket",
850
+ "address": "aws_s3_bucket.secure",
851
+ "change": { "after": { "public": false, "encryption": true } }
852
+ }]
853
+ }
854
+ }
855
+
856
+ test_allow_private_ingress if {
857
+ count(deny) == 0 with input as {
858
+ "resource_changes": [{
859
+ "type": "aws_security_group_rule",
860
+ "change": { "after": { "type": "ingress", "cidr_blocks": ["10.0.0.0/8"] } }
861
+ }]
862
+ }
863
+ }
864
+
865
+ # --- Deny cases (should produce deny) ---
866
+
867
+ test_deny_public_ingress if {
868
+ count(deny) > 0 with input as {
869
+ "resource_changes": [{
870
+ "type": "aws_security_group_rule",
871
+ "change": { "after": { "type": "ingress", "cidr_blocks": ["0.0.0.0/0"] } }
872
+ }]
873
+ }
874
+ }
875
+
876
+ test_deny_public_storage if {
877
+ count(deny) > 0 with input as {
878
+ "resource_changes": [{
879
+ "type": "aws_s3_bucket",
880
+ "address": "aws_s3_bucket.bad",
881
+ "change": { "after": { "public": true, "encryption": false } }
882
+ }]
883
+ }
884
+ }
885
+
886
+ test_deny_unencrypted_database if {
887
+ count(deny) > 0 with input as {
888
+ "resource_changes": [{
889
+ "type": "aws_db_instance",
890
+ "address": "aws_db_instance.bad",
891
+ "change": { "after": { "encryption": false } }
892
+ }]
893
+ }
894
+ }
895
+
896
+ # --- Edge cases ---
897
+
898
+ test_empty_input if {
899
+ count(deny) == 0 with input as {}
900
+ }
901
+
902
+ test_null_resource_changes if {
903
+ count(deny) == 0 with input as { "resource_changes": [] }
904
+ }
905
+
906
+ test_missing_required_fields if {
907
+ count(deny) == 0 with input as { "resource_changes": [{ "type": "unknown_type", "change": {} }] }
908
+ }
909
+ `;
910
+ const testFilePath = selected.path.replace(".rego", "_test.rego");
811
911
  if (runId) {
812
912
  await updateReviewStep(runId, "generate_opa_rego", "approved", {
813
913
  policyPack: selectedPack,
@@ -816,10 +916,14 @@ deny contains msg if {
816
916
  }
817
917
  return asTextResponse({
818
918
  generated_for: { policyPack: selectedPack, cloud: cloud ?? "multi" },
819
- files: [selected],
919
+ files: [
920
+ selected,
921
+ { path: testFilePath, policy: testPolicy, description: "OPA test file — run with: opa test policy/ -v" }
922
+ ],
820
923
  install_notes: [
821
924
  "Run this in CI before deployment apply/admission.",
822
925
  "Fail the pipeline when any deny rules are returned.",
926
+ "Run tests with: opa test policy/ -v",
823
927
  "Version-control the policy and require security-owner approval for policy exceptions."
824
928
  ]
825
929
  });
@@ -868,15 +972,415 @@ tool("security.self_heal_loop", "Propose a human-approved self-healing improveme
868
972
  });
869
973
  }));
870
974
  // ---------------------------------------------------------------------------
975
+ // New tool: security.generate_compliance_report
976
+ // ---------------------------------------------------------------------------
977
+ const GenerateComplianceReportParams = {
978
+ ...ReviewRunIdParam,
979
+ framework: z.enum(["SOC2", "PCI-DSS", "ISO27001", "NIST-800-53", "HIPAA", "GDPR"]).describe("Compliance framework to evaluate against."),
980
+ outputFormat: z.enum(["json", "markdown"]).default("markdown").describe("Output format.")
981
+ };
982
+ const GenerateComplianceReportSchema = z.object(GenerateComplianceReportParams);
983
+ tool("security.generate_compliance_report", "Generate a compliance gap analysis report mapping gate results to a specific framework's controls. Identifies satisfied, missing, and partially-satisfied controls with evidence artifacts.", GenerateComplianceReportParams, safeTool(async (args, _extra) => {
984
+ const { runId, framework, outputFormat } = GenerateComplianceReportSchema.parse(args);
985
+ // Framework → control prefix/tag mapping
986
+ const frameworkFilters = {
987
+ "SOC2": ["SOC2_", "SOC 2"],
988
+ "PCI-DSS": ["PCI_", "PCI DSS"],
989
+ "ISO27001": ["ISO_", "ISO 27001"],
990
+ "NIST-800-53": ["NIST_", "NIST 800-53"],
991
+ "HIPAA": ["HIPAA"],
992
+ "GDPR": ["GDPR"]
993
+ };
994
+ const filters = frameworkFilters[framework] ?? [];
995
+ // Load gate result from run if provided
996
+ let gateFindings = [];
997
+ let gateStatus = "UNKNOWN";
998
+ if (runId) {
999
+ try {
1000
+ const { readReviewRun } = await import("../review/store.js");
1001
+ const run = await readReviewRun(runId);
1002
+ const gateStep = run.steps["run_pr_gate"];
1003
+ if (gateStep?.details) {
1004
+ const details = gateStep.details;
1005
+ gateStatus = String(details["status"] ?? "UNKNOWN");
1006
+ gateFindings = details["findings"] ?? [];
1007
+ }
1008
+ }
1009
+ catch {
1010
+ // run not found — proceed without gate data
1011
+ }
1012
+ }
1013
+ // Load control catalog
1014
+ const { loadControlCatalog } = await import("../gate/catalog.js");
1015
+ const catalog = await loadControlCatalog();
1016
+ // Filter controls by framework
1017
+ const frameworkControls = catalog.controls.filter((c) => filters.some((f) => c.id.startsWith(f) || c.frameworks.some((fw) => fw.includes(f.trim()))));
1018
+ const controlStatuses = frameworkControls.map((c) => {
1019
+ const matchingFinding = gateFindings.find((f) => f.id.startsWith(c.id) || c.id.includes(f.id));
1020
+ if (matchingFinding) {
1021
+ return { id: c.id, description: c.description, status: "missing", evidence: [`Finding: ${matchingFinding.id} (${matchingFinding.severity})`] };
1022
+ }
1023
+ // If no adverse finding, consider it tentatively satisfied
1024
+ return { id: c.id, description: c.description, status: "satisfied", evidence: c.evidence ?? [] };
1025
+ });
1026
+ const total = controlStatuses.length;
1027
+ const satisfied = controlStatuses.filter((c) => c.status === "satisfied").length;
1028
+ const missing = controlStatuses.filter((c) => c.status === "missing").length;
1029
+ const partial = controlStatuses.filter((c) => c.status === "partial").length;
1030
+ if (outputFormat === "json") {
1031
+ return asTextResponse({
1032
+ framework,
1033
+ runId: runId ?? null,
1034
+ gateStatus,
1035
+ summary: { total, satisfied, missing, partial },
1036
+ controls: controlStatuses
1037
+ });
1038
+ }
1039
+ // Markdown output
1040
+ const rows = controlStatuses.map((c) => {
1041
+ const icon = c.status === "satisfied" ? "✓" : c.status === "missing" ? "✗" : "~";
1042
+ const evidence = c.evidence.slice(0, 2).join("; ") || "-";
1043
+ return `| ${c.id} | ${c.description.slice(0, 60)} | ${icon} ${c.status} | ${evidence} |`;
1044
+ }).join("\n");
1045
+ const report = `# Compliance Gap Analysis: ${framework}
1046
+
1047
+ **Run ID**: ${runId ?? "not provided"}
1048
+ **Gate Status**: ${gateStatus}
1049
+ **Generated**: ${new Date().toISOString()}
1050
+
1051
+ ## Summary
1052
+
1053
+ | Metric | Count |
1054
+ |---|---|
1055
+ | Total Controls | ${total} |
1056
+ | Satisfied | ${satisfied} |
1057
+ | Missing | ${missing} |
1058
+ | Partial | ${partial} |
1059
+ | Coverage | ${total > 0 ? Math.round((satisfied / total) * 100) : 0}% |
1060
+
1061
+ ## Control Details
1062
+
1063
+ | Control ID | Description | Status | Evidence |
1064
+ |---|---|---|---|
1065
+ ${rows}
1066
+ `;
1067
+ return asTextResponse(report);
1068
+ }));
1069
+ // ---------------------------------------------------------------------------
1070
+ // New tool: security.notify_webhooks
1071
+ // ---------------------------------------------------------------------------
1072
+ const NotifyWebhooksParams = {
1073
+ runId: z.string().uuid().describe("Security review run ID whose findings to send."),
1074
+ gateFailed: z.boolean().describe("Whether the gate failed (determines alert severity)."),
1075
+ findingCount: z.number().int().describe("Total number of findings."),
1076
+ criticalCount: z.number().int().describe("Number of CRITICAL findings.")
1077
+ };
1078
+ const NotifyWebhooksSchema = z.object(NotifyWebhooksParams);
1079
+ tool("security.notify_webhooks", "Send security gate findings to configured external systems (Slack, Jira, PagerDuty, generic webhook). Configure endpoints via environment variables: SECURITY_SLACK_WEBHOOK, SECURITY_JIRA_URL+SECURITY_JIRA_TOKEN, SECURITY_PAGERDUTY_KEY, SECURITY_WEBHOOK_URL.", NotifyWebhooksParams, safeTool(async (args, _extra) => {
1080
+ const { runId, gateFailed, findingCount, criticalCount } = NotifyWebhooksSchema.parse(args);
1081
+ const notified = [];
1082
+ const errors = [];
1083
+ // Slack
1084
+ const slackWebhook = process.env["SECURITY_SLACK_WEBHOOK"];
1085
+ if (slackWebhook) {
1086
+ try {
1087
+ const color = gateFailed ? "#d32f2f" : "#388e3c";
1088
+ const statusEmoji = gateFailed ? ":red_circle:" : ":large_green_circle:";
1089
+ const body = {
1090
+ blocks: [
1091
+ {
1092
+ type: "header",
1093
+ text: { type: "plain_text", text: `${statusEmoji} Security Gate ${gateFailed ? "FAILED" : "PASSED"}` }
1094
+ },
1095
+ {
1096
+ type: "section",
1097
+ fields: [
1098
+ { type: "mrkdwn", text: `*Run ID*: ${runId}` },
1099
+ { type: "mrkdwn", text: `*Total Findings*: ${findingCount}` },
1100
+ { type: "mrkdwn", text: `*Critical Findings*: ${criticalCount}` }
1101
+ ]
1102
+ }
1103
+ ],
1104
+ attachments: [{ color }]
1105
+ };
1106
+ const controller = new AbortController();
1107
+ const timeout = setTimeout(() => controller.abort(), 10000);
1108
+ try {
1109
+ const resp = await fetch(slackWebhook, {
1110
+ method: "POST",
1111
+ headers: { "Content-Type": "application/json" },
1112
+ body: JSON.stringify(body),
1113
+ signal: controller.signal
1114
+ });
1115
+ if (resp.ok)
1116
+ notified.push("slack");
1117
+ else
1118
+ errors.push(`slack: HTTP ${resp.status}`);
1119
+ }
1120
+ finally {
1121
+ clearTimeout(timeout);
1122
+ }
1123
+ }
1124
+ catch (e) {
1125
+ errors.push(`slack: ${e instanceof Error ? e.message : "unknown error"}`);
1126
+ }
1127
+ }
1128
+ // PagerDuty
1129
+ const pdKey = process.env["SECURITY_PAGERDUTY_KEY"];
1130
+ if (pdKey && gateFailed && criticalCount > 0) {
1131
+ try {
1132
+ const body = {
1133
+ routing_key: pdKey,
1134
+ event_action: "trigger",
1135
+ payload: {
1136
+ summary: `Security Gate FAILED — ${criticalCount} critical findings (run: ${runId})`,
1137
+ severity: "critical",
1138
+ source: "security-mcp",
1139
+ custom_details: { runId, findingCount, criticalCount }
1140
+ }
1141
+ };
1142
+ const controller = new AbortController();
1143
+ const timeout = setTimeout(() => controller.abort(), 10000);
1144
+ try {
1145
+ const resp = await fetch("https://events.pagerduty.com/v2/enqueue", {
1146
+ method: "POST",
1147
+ headers: { "Content-Type": "application/json" },
1148
+ body: JSON.stringify(body),
1149
+ signal: controller.signal
1150
+ });
1151
+ if (resp.ok)
1152
+ notified.push("pagerduty");
1153
+ else
1154
+ errors.push(`pagerduty: HTTP ${resp.status}`);
1155
+ }
1156
+ finally {
1157
+ clearTimeout(timeout);
1158
+ }
1159
+ }
1160
+ catch (e) {
1161
+ errors.push(`pagerduty: ${e instanceof Error ? e.message : "unknown error"}`);
1162
+ }
1163
+ }
1164
+ // Generic webhook
1165
+ const genericWebhook = process.env["SECURITY_WEBHOOK_URL"];
1166
+ if (genericWebhook) {
1167
+ try {
1168
+ const body = { runId, gateFailed, findingCount, criticalCount, timestamp: new Date().toISOString() };
1169
+ const controller = new AbortController();
1170
+ const timeout = setTimeout(() => controller.abort(), 10000);
1171
+ try {
1172
+ const resp = await fetch(genericWebhook, {
1173
+ method: "POST",
1174
+ headers: { "Content-Type": "application/json" },
1175
+ body: JSON.stringify(body),
1176
+ signal: controller.signal
1177
+ });
1178
+ if (resp.ok)
1179
+ notified.push("webhook");
1180
+ else
1181
+ errors.push(`webhook: HTTP ${resp.status}`);
1182
+ }
1183
+ finally {
1184
+ clearTimeout(timeout);
1185
+ }
1186
+ }
1187
+ catch (e) {
1188
+ errors.push(`webhook: ${e instanceof Error ? e.message : "unknown error"}`);
1189
+ }
1190
+ }
1191
+ // Jira
1192
+ const jiraUrl = process.env["SECURITY_JIRA_URL"];
1193
+ const jiraToken = process.env["SECURITY_JIRA_TOKEN"];
1194
+ const jiraProject = process.env["SECURITY_JIRA_PROJECT"] ?? "SECURITY";
1195
+ if (jiraUrl && jiraToken && gateFailed) {
1196
+ try {
1197
+ const body = {
1198
+ fields: {
1199
+ project: { key: jiraProject },
1200
+ summary: `Security Gate FAILED - ${criticalCount} critical findings`,
1201
+ description: {
1202
+ type: "doc",
1203
+ version: 1,
1204
+ content: [{
1205
+ type: "paragraph",
1206
+ content: [{ type: "text", text: `Run ID: ${runId}. Total findings: ${findingCount}. Critical: ${criticalCount}.` }]
1207
+ }]
1208
+ },
1209
+ issuetype: { name: "Bug" },
1210
+ priority: { name: criticalCount > 0 ? "Critical" : "High" }
1211
+ }
1212
+ };
1213
+ const controller = new AbortController();
1214
+ const timeout = setTimeout(() => controller.abort(), 10000);
1215
+ try {
1216
+ const resp = await fetch(`${jiraUrl}/rest/api/3/issue`, {
1217
+ method: "POST",
1218
+ headers: {
1219
+ "Content-Type": "application/json",
1220
+ // Never log the token — pass it only in the header
1221
+ "Authorization": `Bearer ${jiraToken}`
1222
+ },
1223
+ body: JSON.stringify(body),
1224
+ signal: controller.signal
1225
+ });
1226
+ if (resp.ok)
1227
+ notified.push("jira");
1228
+ else
1229
+ errors.push(`jira: HTTP ${resp.status}`);
1230
+ }
1231
+ finally {
1232
+ clearTimeout(timeout);
1233
+ }
1234
+ }
1235
+ catch (e) {
1236
+ errors.push(`jira: ${e instanceof Error ? e.message : "unknown error"}`);
1237
+ }
1238
+ }
1239
+ return asTextResponse({
1240
+ notified,
1241
+ errors,
1242
+ summary: notified.length > 0
1243
+ ? `Notified: ${notified.join(", ")}`
1244
+ : "No webhook integrations configured. Set SECURITY_SLACK_WEBHOOK, SECURITY_PAGERDUTY_KEY, SECURITY_WEBHOOK_URL, or SECURITY_JIRA_URL+SECURITY_JIRA_TOKEN."
1245
+ });
1246
+ }));
1247
+ const REMEDIATION_MAP = {
1248
+ "POSSIBLE_SECRET": {
1249
+ pattern: "const API_KEY = 'sk-...' // hardcoded secret",
1250
+ fix: "const API_KEY = process.env['API_KEY']; // loaded from secret manager",
1251
+ explanation: "Hardcoded secrets are exposed in source control and logs. Load secrets from environment variables backed by a secret manager (AWS Secrets Manager, HashiCorp Vault, etc.).",
1252
+ references: ["CWE-798", "OWASP Top 10 A07:2021", "NIST 800-53 IA-5"]
1253
+ },
1254
+ "CRYPTO_WEAK_HASH": {
1255
+ pattern: "crypto.createHash('md5').update(data).digest('hex')",
1256
+ fix: "crypto.createHash('sha256').update(data).digest('hex')",
1257
+ explanation: "MD5 and SHA-1 are cryptographically broken. Use SHA-256 or higher.",
1258
+ references: ["NIST SP 800-131A Rev 2", "CWE-327"]
1259
+ },
1260
+ "CRYPTO_WEAK_CIPHER": {
1261
+ pattern: "crypto.createCipheriv('des', key, iv)",
1262
+ fix: "crypto.createCipheriv('aes-256-gcm', key, nonce)",
1263
+ explanation: "DES/RC4/3DES are prohibited by NIST. Use AES-256-GCM for authenticated encryption.",
1264
+ references: ["NIST SP 800-131A Rev 2", "CWE-327", "FIPS 140-3"]
1265
+ },
1266
+ "CRYPTO_INSECURE_RANDOM": {
1267
+ pattern: "const token = Math.random().toString(36).slice(2)",
1268
+ fix: "const token = crypto.randomBytes(32).toString('hex')",
1269
+ explanation: "Math.random() is not cryptographically secure. Use crypto.randomBytes() for tokens, keys, and nonces.",
1270
+ references: ["CWE-338", "OWASP ASVS 2.3.1"]
1271
+ },
1272
+ "CRYPTO_WEAK_JWT_ALGO": {
1273
+ pattern: "jwt.sign(payload, secret, { algorithm: 'HS256' })",
1274
+ fix: "jwt.sign(payload, privateKey, { algorithm: 'RS256' })",
1275
+ explanation: "HS256 requires sharing the signing secret with every verifier. RS256/ES256 use asymmetric keys so verifiers only need the public key.",
1276
+ references: ["RFC 7518", "OWASP JWT Security Cheat Sheet"]
1277
+ },
1278
+ "DB_TLS_DISABLED": {
1279
+ pattern: "postgresql://user:pass@host/db?sslmode=disable",
1280
+ fix: "postgresql://user:pass@host/db?sslmode=verify-full",
1281
+ explanation: "Disabling TLS exposes credentials and data in transit. Always require and verify TLS.",
1282
+ references: ["PCI DSS 4.0 Req 4.2", "NIST 800-53 SC-8", "CWE-319"]
1283
+ },
1284
+ "DB_SQL_INJECTION_RISK": {
1285
+ pattern: "db.query('SELECT * FROM users WHERE id = ' + req.params.id)",
1286
+ fix: "db.query('SELECT * FROM users WHERE id = $1', [req.params.id])",
1287
+ explanation: "Never concatenate user input into SQL. Use parameterized queries or ORM query builders.",
1288
+ references: ["OWASP Top 10 A03:2021", "CWE-89", "NIST 800-53 SI-10"]
1289
+ },
1290
+ "GRAPHQL_INTROSPECTION_ENABLED": {
1291
+ pattern: "new ApolloServer({ introspection: true })",
1292
+ fix: "new ApolloServer({ introspection: process.env.NODE_ENV !== 'production' })",
1293
+ explanation: "GraphQL introspection exposes the full schema to attackers. Disable it in non-dev environments.",
1294
+ references: ["OWASP API Security Top 10 API8:2023", "CWE-200"]
1295
+ },
1296
+ "GRAPHQL_NO_DEPTH_LIMIT": {
1297
+ pattern: "new ApolloServer({ schema })",
1298
+ fix: "import depthLimit from 'graphql-depth-limit';\nnew ApolloServer({ schema, validationRules: [depthLimit(10)] })",
1299
+ explanation: "Without depth limiting, attackers can send deeply nested queries to exhaust server resources.",
1300
+ references: ["OWASP API Security Top 10 API4:2023"]
1301
+ },
1302
+ "K8S_PRIVILEGED_CONTAINER": {
1303
+ pattern: "securityContext:\n privileged: true",
1304
+ fix: "securityContext:\n privileged: false\n allowPrivilegeEscalation: false\n runAsNonRoot: true\n capabilities:\n drop: [\"ALL\"]",
1305
+ explanation: "Privileged containers have unrestricted access to the host kernel. Remove privileged mode and drop all capabilities.",
1306
+ references: ["CIS Kubernetes Benchmark 5.2.1", "NIST 800-190"]
1307
+ },
1308
+ "K8S_NO_SECURITY_CONTEXT": {
1309
+ pattern: "containers:\n - name: app\n image: myapp:1.0",
1310
+ fix: "containers:\n - name: app\n image: myapp:1.0\n securityContext:\n runAsNonRoot: true\n runAsUser: 1000\n readOnlyRootFilesystem: true\n allowPrivilegeEscalation: false\n capabilities:\n drop: [\"ALL\"]",
1311
+ explanation: "Always set a securityContext to enforce least-privilege container execution.",
1312
+ references: ["CIS Kubernetes Benchmark", "NIST 800-190", "OWASP Kubernetes Security Cheat Sheet"]
1313
+ },
1314
+ "DLP_REQUEST_BODY_LOGGED": {
1315
+ pattern: "console.log(req.body)",
1316
+ fix: "const { password, token, ...safeFields } = req.body;\nconsole.log({ requestId, safeFields })",
1317
+ explanation: "Full request bodies may contain PII, passwords, or tokens. Log only allowlisted non-sensitive fields.",
1318
+ references: ["GDPR Article 5", "HIPAA 45 CFR 164.312", "CWE-532"]
1319
+ },
1320
+ "DLP_STACK_TRACE_IN_RESPONSE": {
1321
+ pattern: "res.json({ error: err.message, stack: err.stack })",
1322
+ fix: "logger.error({ err, requestId }); // log internally\nres.json({ error: 'An internal error occurred', requestId })",
1323
+ explanation: "Stack traces in API responses disclose internal architecture to attackers (CWE-209). Log internally, return only a safe message.",
1324
+ references: ["CWE-209", "OWASP Top 10 A05:2021", "PCI DSS 4.0 Req 6.2.4"]
1325
+ },
1326
+ "API_TENANT_ID_FROM_INPUT": {
1327
+ pattern: "const tenantId = req.query.tenantId",
1328
+ fix: "const tenantId = req.auth.tenantId // from verified JWT claims",
1329
+ explanation: "Tenant ID must come from the authenticated session/JWT claims. User-supplied tenant IDs allow cross-tenant data access.",
1330
+ references: ["OWASP API Security Top 10 API1:2023", "CWE-639"]
1331
+ },
1332
+ "LOCKFILE_MISSING": {
1333
+ pattern: "# No package-lock.json in repository",
1334
+ fix: "npm install # generates package-lock.json\ngit add package-lock.json\ngit commit -m 'chore: add lockfile'",
1335
+ explanation: "Without a lockfile, npm install resolves the latest matching version on each run, opening the door to supply chain attacks.",
1336
+ references: ["SLSA L1", "NIST 800-218 PS-3", "CWE-829"]
1337
+ },
1338
+ "DEP_FLOATING_VERSION": {
1339
+ pattern: "\"some-package\": \"^1.0.0\"",
1340
+ fix: "\"some-package\": \"1.2.3\" // exact pin\n// or use npm shrinkwrap / lockfile",
1341
+ explanation: "Floating version ranges allow unexpected major/minor updates that may introduce vulnerabilities or breaking changes.",
1342
+ references: ["SLSA L1", "OWASP Top 10 A06:2021"]
1343
+ }
1344
+ };
1345
+ const GenerateRemediationsParams = {
1346
+ findings: z.array(z.object({
1347
+ id: z.string(),
1348
+ title: z.string(),
1349
+ severity: z.string(),
1350
+ files: z.array(z.string()).optional(),
1351
+ evidence: z.array(z.string()).optional()
1352
+ })).describe("Findings array from a gate run result.")
1353
+ };
1354
+ const GenerateRemediationsSchema = z.object(GenerateRemediationsParams);
1355
+ tool("security.generate_remediations", "Maps each gate finding to a specific, actionable code-level remediation template. Called automatically after every gate FAIL. Returns ready-to-apply fix templates keyed by finding ID.", GenerateRemediationsParams, safeTool(async (args, _extra) => {
1356
+ const { findings } = GenerateRemediationsSchema.parse(args);
1357
+ const result = {};
1358
+ for (const finding of findings) {
1359
+ // Try exact match first, then prefix match
1360
+ const exactMatch = REMEDIATION_MAP[finding.id];
1361
+ const prefixMatch = Object.keys(REMEDIATION_MAP).find((k) => finding.id.startsWith(k) || k.startsWith(finding.id));
1362
+ result[finding.id] = {
1363
+ finding,
1364
+ remediation: exactMatch ?? (prefixMatch ? REMEDIATION_MAP[prefixMatch] : null)
1365
+ };
1366
+ }
1367
+ const withRemediation = Object.values(result).filter((r) => r.remediation !== null).length;
1368
+ const without = findings.length - withRemediation;
1369
+ return asTextResponse({
1370
+ summary: { total: findings.length, withRemediation, withoutRemediationTemplate: without },
1371
+ remediations: result
1372
+ });
1373
+ }));
1374
+ // ---------------------------------------------------------------------------
871
1375
  // MCP Prompts capability
872
1376
  // ---------------------------------------------------------------------------
873
- server.prompt("security-engineer", "Activate the security-mcp system prompt. Sets up the model as an elite, threat-informed security engineer applying OWASP, MITRE ATT&CK, NIST 800-53, Zero Trust, PCI DSS, SOC 2, and ISO 27001 to every code and architecture decision.", async () => ({
1377
+ server.prompt("security-engineer", "Activate the security-mcp system prompt. Operating ratio: 90% fixing, 10% advisory — writes the fix, implements the control, enforces the policy. Does NOT list vulnerabilities and walk away. Applies OWASP, MITRE ATT&CK, NIST 800-53, Zero Trust, PCI DSS, SOC 2, and ISO 27001 to every code and architecture decision.", async () => ({
874
1378
  messages: [
875
1379
  {
876
1380
  role: "user",
877
1381
  content: {
878
1382
  type: "text",
879
- text: SECURITY_PROMPT
1383
+ text: getSecurityPrompt()
880
1384
  }
881
1385
  }
882
1386
  ]
@@ -897,6 +1401,54 @@ server.prompt("threat-model-template", "Generate a blank STRIDE + PASTA + MITRE
897
1401
  ]
898
1402
  }));
899
1403
  // ---------------------------------------------------------------------------
1404
+ // Orchestration tools — multi-agent coordination
1405
+ // ---------------------------------------------------------------------------
1406
+ tool("orchestration.create_agent_run", "Initialise a multi-agent orchestration run. Creates the agent-run directory and manifest. Call after security.start_review.", CreateAgentRunSchema.shape, safeTool(async (args, _extra) => {
1407
+ const parsed = CreateAgentRunSchema.parse(args);
1408
+ const result = await createAgentRun(parsed);
1409
+ return asTextResponse(result);
1410
+ }));
1411
+ tool("orchestration.update_agent_status", "Update an agent's lifecycle status (running/completed/completed_partial/failed). Called by each agent at start and end.", UpdateAgentStatusSchema.shape, safeTool(async (args, _extra) => {
1412
+ const parsed = UpdateAgentStatusSchema.parse(args);
1413
+ const result = await updateAgentStatus(parsed);
1414
+ return asTextResponse(result);
1415
+ }));
1416
+ tool("orchestration.merge_agent_findings", "Merge and deduplicate findings from all agents. Sorts by severity (CRITICAL first). Hooks into the attestation flow via updateReviewStep. Call in Phase 3 after all agents complete.", MergeAgentFindingsSchema.shape, safeTool(async (args, _extra) => {
1417
+ const parsed = MergeAgentFindingsSchema.parse(args);
1418
+ const result = await mergeAgentFindings(parsed);
1419
+ return asTextResponse(result);
1420
+ }));
1421
+ tool("orchestration.ensure_skill", "Download a skill from the skills registry if it is not already installed or if it is outdated. Uses the skills-manifest.json registry. Requires internet access.", EnsureSkillSchema.shape, safeTool(async (args, _extra) => {
1422
+ const parsed = EnsureSkillSchema.parse(args);
1423
+ const result = await ensureSkill(parsed);
1424
+ return asTextResponse(result);
1425
+ }));
1426
+ tool("orchestration.read_agent_memory", "Read the persistent memory files for a named agent: patterns, false-positives, remediations, intel, and errors.", ReadAgentMemorySchema.shape, safeTool(async (args, _extra) => {
1427
+ const parsed = ReadAgentMemorySchema.parse(args);
1428
+ const result = await readAgentMemory(parsed);
1429
+ return asTextResponse(result);
1430
+ }));
1431
+ tool("orchestration.write_agent_memory", "Append new entries to an agent's persistent memory (patterns, false-positives, remediations, intel). Memory persists across runs and is used to calibrate findings.", WriteAgentMemorySchema.shape, safeTool(async (args, _extra) => {
1432
+ const parsed = WriteAgentMemorySchema.parse(args);
1433
+ const result = await writeAgentMemory(parsed);
1434
+ return asTextResponse(result);
1435
+ }));
1436
+ tool("orchestration.check_updates", "Check the npm registry and skills manifest for available updates to security-mcp and installed skills.", CheckUpdatesSchema.shape, safeTool(async (args, _extra) => {
1437
+ const parsed = CheckUpdatesSchema.parse(args);
1438
+ const result = await checkUpdates(parsed);
1439
+ return asTextResponse(result);
1440
+ }));
1441
+ tool("orchestration.apply_updates", "Return update commands (choice: manual) or instructions for the agent to run them (choice: auto).", ApplyUpdatesSchema.shape, safeTool(async (args, _extra) => {
1442
+ const parsed = ApplyUpdatesSchema.parse(args);
1443
+ const result = await applyUpdates(parsed);
1444
+ return asTextResponse(result);
1445
+ }));
1446
+ tool("orchestration.verify_skill_coverage", "Verify that all 24 SKILL.md sections have been covered by at least one agent in this run. Returns uncovered sections and a coverage percentage.", VerifySkillCoverageSchema.shape, safeTool(async (args, _extra) => {
1447
+ const parsed = VerifySkillCoverageSchema.parse(args);
1448
+ const result = await verifySkillCoverage(parsed);
1449
+ return asTextResponse(result);
1450
+ }));
1451
+ // ---------------------------------------------------------------------------
900
1452
  // Server startup
901
1453
  // ---------------------------------------------------------------------------
902
1454
  export async function main() {