security-mcp 1.1.4 → 1.3.3

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 (158) hide show
  1. package/README.md +341 -1018
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -0,0 +1,115 @@
1
+ import { writeFile } from "node:fs/promises";
2
+ import fg from "fast-glob";
3
+ import { readFileSafe } from "../../repo/fs.js";
4
+ import { applyEnsures } from "./hcl.js";
5
+ import { detectTerraform } from "./detect.js";
6
+ import { loadCloudRules } from "./types.js";
7
+ const TF_GLOBS = ["**/*.tf"];
8
+ const IGNORE = ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/.claude/**", "src/gate/**"];
9
+ const MAX_ITERATIONS = 500;
10
+ function violationKey(v) {
11
+ return `${v.rule.ruleId}@@${v.file}@@${v.block?.name ?? "?"}`;
12
+ }
13
+ function isAutoApplicable(rule) {
14
+ const s = rule.remediate.strategy;
15
+ if (s === "manual")
16
+ return false;
17
+ if (s === "companion-resource")
18
+ return Boolean(rule.remediate.companion);
19
+ return Boolean(rule.remediate.ensure); // set-attr | insert-block
20
+ }
21
+ /** Apply a single violation's remediation to the document, returning new text (or unchanged). */
22
+ function applyOne(text, v) {
23
+ const { remediate } = v.rule;
24
+ if (remediate.strategy === "companion-resource" && remediate.companion && v.block) {
25
+ const snippet = remediate.companion.replaceAll("${name}", v.block.name);
26
+ const sep = text.endsWith("\n") ? "\n" : "\n\n";
27
+ return text + sep + snippet.trimEnd() + "\n";
28
+ }
29
+ if (remediate.ensure && v.block) {
30
+ return applyEnsures(text, v.block, remediate.ensure);
31
+ }
32
+ return text;
33
+ }
34
+ /** Harden one Terraform document. Returns new text + per-violation outcomes. */
35
+ function hardenText(file, original, rules) {
36
+ let text = original;
37
+ const applied = [];
38
+ const manualMap = new Map();
39
+ const skip = new Set();
40
+ for (let iter = 0; iter < MAX_ITERATIONS; iter++) {
41
+ const violations = detectTerraform(file, text, rules);
42
+ // Record manual / non-applicable violations once.
43
+ for (const v of violations) {
44
+ if (!isAutoApplicable(v.rule))
45
+ manualMap.set(violationKey(v), v);
46
+ }
47
+ const target = violations.find((v) => isAutoApplicable(v.rule) && !skip.has(violationKey(v)));
48
+ if (!target)
49
+ break;
50
+ const key = violationKey(target);
51
+ const candidate = applyOne(text, target);
52
+ if (candidate === text) {
53
+ skip.add(key);
54
+ manualMap.set(key, target);
55
+ continue;
56
+ }
57
+ // Verify the fix actually cleared this violation; otherwise revert + flag manual.
58
+ const after = detectTerraform(file, candidate, rules);
59
+ if (after.some((v) => violationKey(v) === key)) {
60
+ skip.add(key);
61
+ manualMap.set(key, target);
62
+ continue;
63
+ }
64
+ text = candidate;
65
+ applied.push(target);
66
+ }
67
+ return { text, applied, manual: Array.from(manualMap.values()) };
68
+ }
69
+ /**
70
+ * Auto-harden every Terraform file in the working tree against the FSBP/CIS
71
+ * ruleset. Writes changes in place when `write` is true (default). Each applied
72
+ * edit is verified by re-running its own detector before being kept; edits that
73
+ * cannot be applied safely are reported as manual.
74
+ */
75
+ export async function autoHardenTree(opts) {
76
+ const write = opts?.write !== false;
77
+ const rules = await loadCloudRules();
78
+ const report = { applied: [], manual: [], filesChanged: [] };
79
+ if (rules.length === 0)
80
+ return report;
81
+ const files = await fg(TF_GLOBS, { dot: true, followSymbolicLinks: false, ignore: IGNORE });
82
+ for (const file of files) {
83
+ let original = "";
84
+ try {
85
+ original = await readFileSafe(file);
86
+ }
87
+ catch {
88
+ continue;
89
+ }
90
+ const { text, applied, manual } = hardenText(file, original, rules);
91
+ for (const v of applied) {
92
+ report.applied.push({
93
+ ruleId: v.rule.ruleId,
94
+ file,
95
+ resource: `${v.rule.detect.resourceType}.${v.block?.name ?? "?"}`,
96
+ frameworks: v.rule.frameworks
97
+ });
98
+ }
99
+ for (const v of manual) {
100
+ report.manual.push({
101
+ ruleId: v.rule.ruleId,
102
+ file,
103
+ resource: `${v.rule.detect.resourceType}.${v.block?.name ?? "?"}`,
104
+ reason: v.reason,
105
+ snippet: v.rule.remediate.snippet
106
+ });
107
+ }
108
+ if (text !== original) {
109
+ report.filesChanged.push(file);
110
+ if (write)
111
+ await writeFile(file, text, "utf-8");
112
+ }
113
+ }
114
+ return report;
115
+ }
@@ -0,0 +1,36 @@
1
+ // Bicep resource extraction. Block-structured like HCL, so we reuse the
2
+ // brace-balanced matcher to capture each `resource <name> '<type>@<ver>' = { ... }`
3
+ // body. Detect-only — no auto-fix is attempted for Bicep.
4
+ import { matchBrace } from "./hcl.js";
5
+ // resource <symbolicName> '<type>@<apiVersion>' = [if (...)] { ... }
6
+ const RESOURCE_HEADER = /resource\s+([A-Za-z_][A-Za-z0-9_]*)\s+'([^']+)'\s*=\s*(?:if\s*\([^)]*\)\s*)?\{/g;
7
+ function lineOf(text, index) {
8
+ let line = 1;
9
+ for (let i = 0; i < index && i < text.length; i++) {
10
+ if (text[i] === "\n")
11
+ line++;
12
+ }
13
+ return line;
14
+ }
15
+ /** Parse all Bicep `resource` declarations, stripping the `@apiVersion` suffix from the type. */
16
+ export function parseBicepResources(text) {
17
+ const out = [];
18
+ RESOURCE_HEADER.lastIndex = 0;
19
+ let m;
20
+ while ((m = RESOURCE_HEADER.exec(text)) !== null) {
21
+ const open = text.indexOf("{", m.index);
22
+ if (open < 0)
23
+ continue;
24
+ const close = matchBrace(text, open);
25
+ if (close < 0)
26
+ continue;
27
+ out.push({
28
+ type: m[2].split("@")[0],
29
+ name: m[1],
30
+ body: text.slice(open + 1, close),
31
+ line: lineOf(text, m.index)
32
+ });
33
+ RESOURCE_HEADER.lastIndex = close + 1;
34
+ }
35
+ return out;
36
+ }
@@ -0,0 +1,125 @@
1
+ // CloudFormation / SAM template resource extraction (JSON and YAML).
2
+ //
3
+ // Detect-only: we locate each `Resources` entry and capture its text so a rule's
4
+ // forbid/require regex can be scoped to one resource. Heuristic — YAML is parsed
5
+ // by indentation, not a full YAML engine, so unusual layouts may be missed. No
6
+ // auto-fix is attempted for CloudFormation (JSON re-serialize drops comments;
7
+ // YAML anchors are unsafe to rewrite), so detection accuracy is sufficient.
8
+ const CFN_TYPE = /Type\s*:\s*['"]?(AWS::[A-Za-z0-9]+::[A-Za-z0-9]+)/;
9
+ /** Cheap pre-filter so we don't JSON.parse / scan every json/yaml in the repo. */
10
+ export function looksLikeCfn(text) {
11
+ return (text.includes("AWSTemplateFormatVersion") ||
12
+ text.includes("AWS::Serverless") ||
13
+ /["']?Type["']?\s*:\s*["']?AWS::/.test(text));
14
+ }
15
+ function lineOf(text, index) {
16
+ let line = 1;
17
+ for (let i = 0; i < index && i < text.length; i++) {
18
+ if (text[i] === "\n")
19
+ line++;
20
+ }
21
+ return line;
22
+ }
23
+ function escapeRegex(s) {
24
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
25
+ }
26
+ function asRecord(v) {
27
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
28
+ }
29
+ function parseJsonCfn(text) {
30
+ let doc;
31
+ try {
32
+ doc = JSON.parse(text);
33
+ }
34
+ catch {
35
+ return [];
36
+ }
37
+ const root = asRecord(doc);
38
+ const resources = root && asRecord(root["Resources"]);
39
+ if (!resources)
40
+ return [];
41
+ const out = [];
42
+ for (const [name, resVal] of Object.entries(resources)) {
43
+ const res = asRecord(resVal);
44
+ const type = res?.["Type"];
45
+ if (typeof type !== "string" || !type.startsWith("AWS::"))
46
+ continue;
47
+ const re = new RegExp(`"${escapeRegex(name)}"\\s*:`);
48
+ const m = re.exec(text);
49
+ out.push({ type, name, body: JSON.stringify(res), line: m ? lineOf(text, m.index) : 1 });
50
+ }
51
+ return out;
52
+ }
53
+ function indentOf(line) {
54
+ return line.length - line.trimStart().length;
55
+ }
56
+ function findResourcesIndent(lines) {
57
+ for (let i = 0; i < lines.length; i++) {
58
+ const m = /^(\s*)Resources\s*:\s*$/.exec(lines[i]);
59
+ if (m)
60
+ return { start: i, indent: m[1].length };
61
+ }
62
+ return null;
63
+ }
64
+ function parseYamlCfn(text) {
65
+ const lines = text.split("\n");
66
+ const res = findResourcesIndent(lines);
67
+ if (!res)
68
+ return [];
69
+ // First logical-id sits at the indent level directly under "Resources:".
70
+ let childIndent = -1;
71
+ for (let i = res.start + 1; i < lines.length; i++) {
72
+ if (!lines[i].trim() || /^\s*#/.test(lines[i]))
73
+ continue;
74
+ const ind = indentOf(lines[i]);
75
+ if (ind <= res.indent)
76
+ return [];
77
+ childIndent = ind;
78
+ break;
79
+ }
80
+ if (childIndent < 0)
81
+ return [];
82
+ const out = [];
83
+ let name = "";
84
+ let start = -1;
85
+ let body = [];
86
+ const flush = () => {
87
+ if (!name)
88
+ return;
89
+ const text2 = body.join("\n");
90
+ const tm = CFN_TYPE.exec(text2);
91
+ if (tm)
92
+ out.push({ type: tm[1], name, body: text2, line: start + 1 });
93
+ name = "";
94
+ body = [];
95
+ };
96
+ for (let i = res.start + 1; i < lines.length; i++) {
97
+ const l = lines[i];
98
+ if (!l.trim()) {
99
+ if (name)
100
+ body.push(l);
101
+ continue;
102
+ }
103
+ const ind = indentOf(l);
104
+ if (ind <= res.indent) {
105
+ flush();
106
+ break;
107
+ }
108
+ if (ind === childIndent) {
109
+ const hm = /^\s*([A-Za-z0-9_]+)\s*:/.exec(l);
110
+ flush();
111
+ name = hm ? hm[1] : "";
112
+ start = i;
113
+ body = [l];
114
+ }
115
+ else if (name) {
116
+ body.push(l);
117
+ }
118
+ }
119
+ flush();
120
+ return out;
121
+ }
122
+ /** Parse all CloudFormation resources from a JSON or YAML template. */
123
+ export function parseCfnResources(text) {
124
+ return text.trimStart().startsWith("{") ? parseJsonCfn(text) : parseYamlCfn(text);
125
+ }
@@ -0,0 +1,104 @@
1
+ import { parseResourceBlocks } from "./hcl.js";
2
+ import { looksLikeCfn, parseCfnResources } from "./cfn.js";
3
+ import { parseBicepResources } from "./bicep.js";
4
+ function lineOf(text, index) {
5
+ let line = 1;
6
+ for (let i = 0; i < index && i < text.length; i++) {
7
+ if (text[i] === "\n")
8
+ line++;
9
+ }
10
+ return line;
11
+ }
12
+ function blockBody(text, block) {
13
+ return text.slice(block.bodyStart, block.bodyEnd);
14
+ }
15
+ function compile(pattern) {
16
+ return new RegExp(pattern, "i");
17
+ }
18
+ /**
19
+ * Does the file contain a resource of `companionType` that references
20
+ * `origType.name` (the offending resource)? Used for cross-resource rules such
21
+ * as an S3 bucket needing a matching aws_s3_bucket_public_access_block.
22
+ */
23
+ function companionExists(blocks, fullText, companionType, origType, name) {
24
+ const ref = `${origType}.${name}`;
25
+ return blocks.some((b) => b.type === companionType && blockBody(fullText, b).includes(ref));
26
+ }
27
+ /** Evaluate every terraform-target rule against one parsed HCL document. */
28
+ export function detectTerraform(file, text, rules) {
29
+ const blocks = parseResourceBlocks(text);
30
+ if (blocks.length === 0)
31
+ return [];
32
+ const violations = [];
33
+ for (const rule of rules) {
34
+ if (rule.detect.target !== "terraform")
35
+ continue;
36
+ const { resourceType, forbid, require, requireCompanionType } = rule.detect;
37
+ const forbidRe = forbid ? compile(forbid) : null;
38
+ const requireRe = require ? compile(require) : null;
39
+ for (const block of blocks) {
40
+ if (block.type !== resourceType)
41
+ continue;
42
+ const body = blockBody(text, block);
43
+ const line = lineOf(text, block.start);
44
+ if (forbidRe && forbidRe.test(body)) {
45
+ violations.push({ rule, file, line, block, reason: "insecure value present" });
46
+ continue;
47
+ }
48
+ if (requireRe && !requireRe.test(body)) {
49
+ violations.push({ rule, file, line, block, reason: "secure setting missing" });
50
+ continue;
51
+ }
52
+ if (requireCompanionType &&
53
+ !companionExists(blocks, text, requireCompanionType, resourceType, block.name)) {
54
+ violations.push({
55
+ rule,
56
+ file,
57
+ line,
58
+ block,
59
+ reason: `missing companion ${requireCompanionType}`
60
+ });
61
+ }
62
+ }
63
+ }
64
+ return violations;
65
+ }
66
+ /**
67
+ * Evaluate forbid/require rules for one target against a list of already-parsed
68
+ * resources (CloudFormation or Bicep). Body-scoped regex, detect-only.
69
+ */
70
+ function detectResources(file, rules, target, resources) {
71
+ if (resources.length === 0)
72
+ return [];
73
+ const violations = [];
74
+ for (const rule of rules) {
75
+ if (rule.detect.target !== target)
76
+ continue;
77
+ const { resourceType, forbid, require } = rule.detect;
78
+ const forbidRe = forbid ? compile(forbid) : null;
79
+ const requireRe = require ? compile(require) : null;
80
+ for (const res of resources) {
81
+ if (res.type !== resourceType)
82
+ continue;
83
+ if (forbidRe && forbidRe.test(res.body)) {
84
+ violations.push({ rule, file, line: res.line, reason: "insecure value present" });
85
+ }
86
+ else if (requireRe && !requireRe.test(res.body)) {
87
+ violations.push({ rule, file, line: res.line, reason: "secure setting missing" });
88
+ }
89
+ }
90
+ }
91
+ return violations;
92
+ }
93
+ /** Evaluate cloudformation-target rules against a JSON or YAML template. */
94
+ export function detectCloudFormation(file, text, rules) {
95
+ if (!looksLikeCfn(text))
96
+ return [];
97
+ const resources = parseCfnResources(text);
98
+ return detectResources(file, rules, "cloudformation", resources);
99
+ }
100
+ /** Evaluate bicep-target rules against a Bicep document. */
101
+ export function detectBicep(file, text, rules) {
102
+ const resources = parseBicepResources(text);
103
+ return detectResources(file, rules, "bicep", resources);
104
+ }
@@ -0,0 +1,140 @@
1
+ // Lightweight, dependency-free HCL (Terraform) block utilities.
2
+ //
3
+ // These are intentionally heuristic — brace-balanced scanning, not a full HCL
4
+ // parser. They are robust enough to locate `resource "type" "name" { ... }`
5
+ // blocks and apply scoped attribute edits inside a single block, which is all
6
+ // the detect-and-remediate engine needs. Edge cases (heredocs, unusual
7
+ // formatting) may be missed; such rules degrade to "manual" remediation.
8
+ const RESOURCE_HEADER = /resource\s+"([^"]+)"\s+"([^"]+)"\s*\{/g;
9
+ /**
10
+ * Find the index of the "}" matching the "{" at openIdx. Skips braces that
11
+ * appear inside double-quoted strings and line comments (# and //).
12
+ * Returns -1 if unbalanced.
13
+ */
14
+ export function matchBrace(text, openIdx) {
15
+ let depth = 0;
16
+ let inString = false;
17
+ let inLineComment = false;
18
+ for (let i = openIdx; i < text.length; i++) {
19
+ const ch = text[i];
20
+ if (inLineComment) {
21
+ if (ch === "\n")
22
+ inLineComment = false;
23
+ continue;
24
+ }
25
+ if (inString) {
26
+ if (ch === "\\") {
27
+ i++; // skip escaped char
28
+ }
29
+ else if (ch === '"') {
30
+ inString = false;
31
+ }
32
+ continue;
33
+ }
34
+ if (ch === '"') {
35
+ inString = true;
36
+ }
37
+ else if (ch === "#") {
38
+ inLineComment = true;
39
+ }
40
+ else if (ch === "/" && text[i + 1] === "/") {
41
+ inLineComment = true;
42
+ i++;
43
+ }
44
+ else if (ch === "{") {
45
+ depth++;
46
+ }
47
+ else if (ch === "}") {
48
+ depth--;
49
+ if (depth === 0)
50
+ return i;
51
+ }
52
+ }
53
+ return -1;
54
+ }
55
+ /** Parse all top-level `resource "type" "name"` blocks in a Terraform document. */
56
+ export function parseResourceBlocks(text) {
57
+ const blocks = [];
58
+ RESOURCE_HEADER.lastIndex = 0;
59
+ let m;
60
+ while ((m = RESOURCE_HEADER.exec(text)) !== null) {
61
+ const openBrace = text.indexOf("{", m.index);
62
+ if (openBrace < 0)
63
+ continue;
64
+ const close = matchBrace(text, openBrace);
65
+ if (close < 0)
66
+ continue;
67
+ blocks.push({
68
+ type: m[1],
69
+ name: m[2],
70
+ start: m.index,
71
+ end: close + 1,
72
+ bodyStart: openBrace + 1,
73
+ bodyEnd: close
74
+ });
75
+ RESOURCE_HEADER.lastIndex = close + 1;
76
+ }
77
+ return blocks;
78
+ }
79
+ /**
80
+ * Ensure `attr = value` exists inside an HCL body string for a single-segment
81
+ * attribute. Replaces an existing assignment (even if the value differs) or
82
+ * inserts a new one at the top of the body. Returns the new body string.
83
+ */
84
+ function ensureLeafAttr(body, attr, value, indent) {
85
+ const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
86
+ const assignRe = new RegExp(`^([ \\t]*)${escaped}(\\s*)=.*$`, "m");
87
+ if (assignRe.test(body)) {
88
+ return body.replace(assignRe, `$1${attr} = ${value}`);
89
+ }
90
+ const leading = body.startsWith("\n") ? "\n" : "";
91
+ const rest = body.startsWith("\n") ? body.slice(1) : body;
92
+ return `${leading}${indent}${attr} = ${value}\n${rest}`;
93
+ }
94
+ /** Build a fresh nested block chain `a { b { leaf = value } }` for a missing path. */
95
+ function buildNestedChain(segs, value, indent) {
96
+ const [head, ...rest] = segs;
97
+ if (rest.length === 0) {
98
+ return `${indent}${head} = ${value}\n`;
99
+ }
100
+ const inner = buildNestedChain(rest, value, indent + " ");
101
+ return `${indent}${head} {\n${inner}${indent}}\n`;
102
+ }
103
+ /**
104
+ * Ensure a nested-path attribute `a.b.c = value` inside an HCL body, creating
105
+ * any missing intermediate `block { ... }` levels. Handles arbitrary depth.
106
+ */
107
+ function ensurePath(body, segs, value, indent) {
108
+ if (segs.length === 1) {
109
+ return ensureLeafAttr(body, segs[0], value, indent);
110
+ }
111
+ const [parent, ...rest] = segs;
112
+ const escaped = parent.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
113
+ const headerRe = new RegExp(`(^|\\n)([ \\t]*)${escaped}\\s*\\{`);
114
+ const match = headerRe.exec(body);
115
+ if (match) {
116
+ const openBrace = body.indexOf("{", match.index + match[1].length);
117
+ const close = matchBrace(body, openBrace);
118
+ if (openBrace >= 0 && close >= 0) {
119
+ const innerBody = body.slice(openBrace + 1, close);
120
+ const newInner = ensurePath(innerBody, rest, value, indent + " ");
121
+ return body.slice(0, openBrace + 1) + newInner + body.slice(close);
122
+ }
123
+ }
124
+ const block = buildNestedChain(segs, value, indent);
125
+ const leading = body.startsWith("\n") ? "\n" : "";
126
+ const restBody = body.startsWith("\n") ? body.slice(1) : body;
127
+ return `${leading}${block}${restBody}`;
128
+ }
129
+ /**
130
+ * Apply a set of dotted-path attribute assignments to one resource block in
131
+ * fullText, returning the rewritten document. Supports depth 1 (attr) and 2
132
+ * (parent.child). Unknown depths are skipped.
133
+ */
134
+ export function applyEnsures(fullText, block, ensure) {
135
+ let body = fullText.slice(block.bodyStart, block.bodyEnd);
136
+ for (const [path, value] of Object.entries(ensure)) {
137
+ body = ensurePath(body, path.split("."), value, " ");
138
+ }
139
+ return fullText.slice(0, block.bodyStart) + body + fullText.slice(block.bodyEnd);
140
+ }
@@ -0,0 +1,87 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ // src/gate/cloud-controls -> repo root is three levels up (dist/gate/cloud-controls at runtime).
7
+ const PKG_ROOT = resolve(__dirname, "../../..");
8
+ export const CloudProviderSchema = z.enum(["aws", "gcp", "azure"]);
9
+ const DetectSchema = z.object({
10
+ // How the rule body is matched:
11
+ // "terraform" — HCL resource blocks (supports auto-fix).
12
+ // "cloudformation" — CloudFormation/SAM resources in JSON or YAML (detect-only).
13
+ // "bicep" — Bicep resource declarations (detect-only).
14
+ // Only "terraform" supports auto-remediation; the others are emit-and-fix-manually.
15
+ target: z.enum(["terraform", "cloudformation", "bicep"]),
16
+ // Resource type for the target: Terraform "aws_instance", CloudFormation
17
+ // "AWS::S3::Bucket", or Bicep "Microsoft.Storage/storageAccounts".
18
+ resourceType: z.string(),
19
+ // Regex; if it matches inside the resource block the resource is INSECURE.
20
+ forbid: z.string().optional(),
21
+ // Regex; if it is ABSENT from the resource block the resource is insecure-by-omission.
22
+ require: z.string().optional(),
23
+ // Cross-resource: the resource is insecure unless a companion resource of this
24
+ // Terraform type exists in the same file and references it by local name.
25
+ requireCompanionType: z.string().optional()
26
+ });
27
+ const RemediateSchema = z.object({
28
+ strategy: z.enum(["set-attr", "insert-block", "companion-resource", "manual"]),
29
+ // Dotted attribute path -> raw HCL value literal. Depth up to 2 (parent.child).
30
+ // e.g. { "metadata_options.http_tokens": "\"required\"" }.
31
+ ensure: z.record(z.string(), z.string()).optional(),
32
+ // Companion resource HCL template. "${name}" is substituted with the offending
33
+ // resource's local name. Used by strategy "companion-resource".
34
+ companion: z.string().optional(),
35
+ // Hardened snippet / guidance emitted when the fix cannot be applied automatically.
36
+ snippet: z.string().optional()
37
+ });
38
+ const RuleSchema = z.object({
39
+ ruleId: z.string(),
40
+ // The attack this misconfiguration enables — why it matters, not "it's non-compliant".
41
+ threat: z.string(),
42
+ // Framework labels for context only, e.g. ["AWS FSBP EC2.8", "CIS AWS Foundations Benchmark 5.6"].
43
+ frameworks: z.array(z.string()).default([]),
44
+ severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
45
+ title: z.string(),
46
+ detect: DetectSchema,
47
+ remediate: RemediateSchema,
48
+ requiredActions: z.array(z.string()).min(1)
49
+ });
50
+ const RegistrySchema = z.object({
51
+ version: z.string(),
52
+ rules: z.array(RuleSchema)
53
+ });
54
+ const PROVIDER_FILES = {
55
+ aws: "defaults/cloud-controls/aws.json",
56
+ gcp: "defaults/cloud-controls/gcp.json",
57
+ azure: "defaults/cloud-controls/azure.json"
58
+ };
59
+ async function loadProvider(cloud) {
60
+ const path = resolve(PKG_ROOT, PROVIDER_FILES[cloud]);
61
+ let raw;
62
+ try {
63
+ raw = await readFile(path, "utf-8");
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ const parsed = RegistrySchema.parse(JSON.parse(raw));
69
+ return parsed.rules.map((rule) => ({ ...rule, cloud }));
70
+ }
71
+ /** Load every cloud-control rule across all providers, tagged with its cloud. */
72
+ export async function loadCloudRules(providers) {
73
+ const list = providers ?? ["aws", "gcp", "azure"];
74
+ const groups = await Promise.all(list.map(loadProvider));
75
+ const seen = new Set();
76
+ const rules = [];
77
+ for (const group of groups) {
78
+ for (const rule of group) {
79
+ if (seen.has(rule.ruleId)) {
80
+ throw new Error(`Duplicate cloud-control ruleId: ${rule.ruleId}`);
81
+ }
82
+ seen.add(rule.ruleId);
83
+ rules.push(rule);
84
+ }
85
+ }
86
+ return rules;
87
+ }
package/dist/gate/diff.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execa } from "execa";
2
+ import { access } from "node:fs/promises";
2
3
  // Allowlist for git ref strings. Blocks option injection (e.g. --upload-pack=…)
3
4
  // and git pathspec magic characters. CWE-88 / MITRE ATT&CK T1059.
4
5
  // Note: ~ and ^ are intentionally included — they are safe because { and } are NOT
@@ -12,12 +13,23 @@ function validateRef(name, value) {
12
13
  export async function getChangedFiles(opts) {
13
14
  validateRef("baseRef", opts.baseRef);
14
15
  validateRef("headRef", opts.headRef);
15
- // Uses git diff in CI. Assumes checkout has full history for baseRef.
16
- const { stdout } = await execa("git", ["diff", "--name-only", `${opts.baseRef}...${opts.headRef}`], {
17
- stdio: ["ignore", "pipe", "pipe"]
18
- });
19
- return stdout
16
+ // Fix 9: --diff-filter=ACMRT excludes deleted-only files; -M detects renames
17
+ // so renamed files appear as renames rather than delete+add pairs.
18
+ const { stdout } = await execa("git", ["diff", "--diff-filter=ACMRT", "-M", "--name-only", `${opts.baseRef}...${opts.headRef}`], { stdio: ["ignore", "pipe", "pipe"] });
19
+ const candidates = stdout
20
20
  .split("\n")
21
21
  .map((s) => s.trim())
22
22
  .filter(Boolean);
23
+ // Fix 9: skip any file that no longer exists on disk (deleted/moved away edge cases)
24
+ const results = [];
25
+ for (const file of candidates) {
26
+ try {
27
+ await access(file);
28
+ results.push(file);
29
+ }
30
+ catch {
31
+ // file does not exist on disk — skip gracefully
32
+ }
33
+ }
34
+ return results;
23
35
  }
@@ -8,7 +8,14 @@ const PKG_ROOT = resolve(__dirname, "../..");
8
8
  async function loadEvidenceMap() {
9
9
  const overridePath = process.env["SECURITY_GATE_EVIDENCE_MAP"];
10
10
  if (overridePath) {
11
- const raw = await readFile(join(process.cwd(), overridePath), "utf-8");
11
+ // Guard against path traversal (VULN-002 / CWE-22): join() normalises '..' sequences
12
+ // but does NOT prevent escape; resolve() + startsWith() is the correct check.
13
+ const cwd = process.cwd();
14
+ const resolved = resolve(cwd, overridePath);
15
+ if (resolved !== cwd && !resolved.startsWith(cwd + "/")) {
16
+ throw new Error("SECURITY_GATE_EVIDENCE_MAP path escapes the project directory");
17
+ }
18
+ const raw = await readFile(resolved, "utf-8");
12
19
  return JSON.parse(raw);
13
20
  }
14
21
  try {