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.
- package/README.md +963 -193
- package/defaults/agent-run-schema.json +98 -0
- package/defaults/checklists/ai.json +25 -0
- package/defaults/checklists/api.json +27 -0
- package/defaults/checklists/infra.json +27 -0
- package/defaults/checklists/mobile.json +25 -0
- package/defaults/checklists/payments.json +25 -0
- package/defaults/checklists/web.json +30 -0
- package/defaults/control-catalog.json +392 -0
- package/defaults/evidence-map.json +194 -0
- package/defaults/security-policy.json +41 -2
- package/dist/cli/index.js +13 -8
- package/dist/cli/install.js +80 -2
- package/dist/cli/onboarding.js +590 -0
- package/dist/cli/update.js +83 -15
- package/dist/gate/baseline.js +115 -0
- package/dist/gate/checks/ai-redteam.js +398 -0
- package/dist/gate/checks/api.js +93 -0
- package/dist/gate/checks/crypto.js +153 -0
- package/dist/gate/checks/database.js +144 -0
- package/dist/gate/checks/dependencies.js +126 -0
- package/dist/gate/checks/dlp.js +153 -0
- package/dist/gate/checks/graphql.js +122 -0
- package/dist/gate/checks/infra.js +126 -12
- package/dist/gate/checks/k8s.js +190 -0
- package/dist/gate/checks/playbook.js +160 -0
- package/dist/gate/checks/runtime.js +316 -0
- package/dist/gate/checks/sbom.js +199 -0
- package/dist/gate/checks/scanners.js +379 -8
- package/dist/gate/checks/secrets.js +85 -20
- package/dist/gate/exceptions.js +6 -1
- package/dist/gate/policy.js +85 -19
- package/dist/gate/threat-intel.js +157 -0
- package/dist/mcp/orchestration.js +586 -0
- package/dist/mcp/server.js +568 -16
- package/dist/repo/search.js +11 -1
- package/dist/review/store.js +133 -0
- package/dist/types/agent-run.js +8 -0
- package/package.json +5 -5
- package/prompts/SECURITY_PROMPT.md +415 -1
- package/skills/agentic-loop-exploiter/SKILL.md +69 -0
- package/skills/ai-llm-redteam/SKILL.md +118 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +85 -0
- package/skills/android-penetration-tester/SKILL.md +83 -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/business-logic-attacker/SKILL.md +76 -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/crypto-pki-specialist/SKILL.md +136 -0
- package/skills/dependency-confusion-attacker/SKILL.md +78 -0
- package/skills/evidence-collector/SKILL.md +86 -0
- package/skills/gcp-penetration-tester/SKILL.md +63 -0
- package/skills/injection-specialist/SKILL.md +62 -0
- package/skills/ios-security-auditor/SKILL.md +77 -0
- package/skills/k8s-container-escaper/SKILL.md +74 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +92 -0
- package/skills/logic-race-fuzzer/SKILL.md +67 -0
- package/skills/mobile-api-network-attacker/SKILL.md +81 -0
- package/skills/mobile-security-specialist/SKILL.md +124 -0
- package/skills/model-extraction-attacker/SKILL.md +68 -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/rag-poisoning-specialist/SKILL.md +71 -0
- package/skills/senior-security-engineer/SKILL.md +75 -13
- package/skills/serialization-memory-attacker/SKILL.md +78 -0
- package/skills/stride-pasta-analyst/SKILL.md +72 -0
- package/skills/supply-chain-devsecops/SKILL.md +82 -0
- package/skills/threat-modeler/SKILL.md +116 -0
- package/skills/tls-certificate-auditor/SKILL.md +76 -0
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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()
|
|
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
|
-
|
|
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: [
|
|
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.
|
|
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:
|
|
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() {
|