codegate-ai 0.6.1 → 0.8.0

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 (45) hide show
  1. package/README.md +61 -25
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +59 -41
  4. package/dist/commands/scan-command/helpers.d.ts +6 -1
  5. package/dist/commands/scan-command/helpers.js +46 -1
  6. package/dist/commands/scan-command.js +49 -55
  7. package/dist/commands/scan-content-command.d.ts +16 -0
  8. package/dist/commands/scan-content-command.js +61 -0
  9. package/dist/config/suppression-policy.d.ts +14 -0
  10. package/dist/config/suppression-policy.js +81 -0
  11. package/dist/config.d.ts +5 -0
  12. package/dist/config.js +29 -3
  13. package/dist/layer2-static/advisories/agent-components.json +62 -0
  14. package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
  15. package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
  16. package/dist/layer2-static/detectors/command-exec.js +6 -0
  17. package/dist/layer2-static/detectors/rule-file.js +5 -0
  18. package/dist/layer2-static/engine.d.ts +4 -1
  19. package/dist/layer2-static/engine.js +97 -0
  20. package/dist/layer2-static/rule-engine.d.ts +1 -1
  21. package/dist/layer2-static/rule-engine.js +1 -13
  22. package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
  23. package/dist/layer2-static/rule-pack-loader.js +187 -0
  24. package/dist/layer3-dynamic/command-builder.d.ts +1 -0
  25. package/dist/layer3-dynamic/command-builder.js +44 -2
  26. package/dist/layer3-dynamic/local-text-analysis.d.ts +9 -1
  27. package/dist/layer3-dynamic/local-text-analysis.js +12 -27
  28. package/dist/layer3-dynamic/meta-agent.d.ts +1 -2
  29. package/dist/layer3-dynamic/meta-agent.js +3 -6
  30. package/dist/layer3-dynamic/prompt-templates/local-text-analysis.md +33 -21
  31. package/dist/layer3-dynamic/prompt-templates/security-analysis.md +11 -1
  32. package/dist/layer3-dynamic/prompt-templates/tool-poisoning.md +9 -1
  33. package/dist/layer3-dynamic/toxic-flow.js +6 -0
  34. package/dist/pipeline.js +9 -8
  35. package/dist/report/finding-fingerprint.d.ts +5 -0
  36. package/dist/report/finding-fingerprint.js +47 -0
  37. package/dist/reporter/markdown.js +25 -3
  38. package/dist/reporter/sarif.js +2 -0
  39. package/dist/reporter/terminal.js +25 -0
  40. package/dist/scan-target/fetch-plan.d.ts +8 -0
  41. package/dist/scan-target/fetch-plan.js +30 -0
  42. package/dist/scan-target/staging.js +60 -5
  43. package/dist/scan.js +3 -0
  44. package/dist/types/finding.d.ts +9 -0
  45. package/package.json +3 -1
@@ -0,0 +1,81 @@
1
+ function normalizeString(value) {
2
+ if (typeof value !== "string") {
3
+ return undefined;
4
+ }
5
+ const trimmed = value.trim();
6
+ return trimmed.length > 0 ? trimmed : undefined;
7
+ }
8
+ function escapeRegExp(value) {
9
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10
+ }
11
+ function globToRegExp(glob) {
12
+ const pattern = normalizeString(glob)?.replaceAll("\\", "/");
13
+ if (!pattern) {
14
+ return /^$/;
15
+ }
16
+ let regex = "^";
17
+ for (let index = 0; index < pattern.length; index++) {
18
+ const char = pattern[index];
19
+ if (char === "*") {
20
+ if (pattern[index + 1] === "*") {
21
+ regex += ".*";
22
+ index++;
23
+ }
24
+ else {
25
+ regex += "[^/]*";
26
+ }
27
+ continue;
28
+ }
29
+ if (char === "?") {
30
+ regex += "[^/]";
31
+ continue;
32
+ }
33
+ regex += escapeRegExp(char);
34
+ }
35
+ regex += "$";
36
+ return new RegExp(regex);
37
+ }
38
+ function matchesGlob(value, glob) {
39
+ return globToRegExp(glob).test(value.replaceAll("\\", "/"));
40
+ }
41
+ function matchesSuppressionRule(finding, rule) {
42
+ const ruleId = normalizeString(rule.rule_id);
43
+ if (ruleId && finding.rule_id !== ruleId) {
44
+ return false;
45
+ }
46
+ const filePath = normalizeString(rule.file_path);
47
+ if (filePath && !matchesGlob(finding.file_path, filePath)) {
48
+ return false;
49
+ }
50
+ const severity = rule.severity;
51
+ if (severity && finding.severity !== severity) {
52
+ return false;
53
+ }
54
+ const category = normalizeString(rule.category);
55
+ if (category && finding.category !== category) {
56
+ return false;
57
+ }
58
+ const cwe = normalizeString(rule.cwe);
59
+ if (cwe && finding.cwe !== cwe) {
60
+ return false;
61
+ }
62
+ const fingerprint = normalizeString(rule.fingerprint);
63
+ if (fingerprint && finding.fingerprint !== fingerprint) {
64
+ return false;
65
+ }
66
+ return true;
67
+ }
68
+ export function applySuppressionPolicy(findings, policy) {
69
+ const legacySuppressions = new Set((policy.suppress_findings ?? [])
70
+ .map((findingId) => normalizeString(findingId))
71
+ .filter((findingId) => findingId !== undefined));
72
+ const rules = policy.suppression_rules ?? [];
73
+ return findings.map((finding) => {
74
+ const ruleMatch = rules.some((rule) => matchesSuppressionRule(finding, rule));
75
+ const legacyMatch = legacySuppressions.has(finding.finding_id);
76
+ return {
77
+ ...finding,
78
+ suppressed: finding.suppressed || legacyMatch || ruleMatch,
79
+ };
80
+ });
81
+ }
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Finding } from "./types/finding.js";
2
2
  import type { CodeGateReport } from "./types/report.js";
3
+ import { type SuppressionRule } from "./config/suppression-policy.js";
3
4
  export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "sarif", "markdown", "html"];
4
5
  export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
5
6
  export declare const SEVERITY_THRESHOLDS: readonly ["critical", "high", "medium", "low", "info"];
@@ -32,7 +33,11 @@ export interface CodeGateConfig {
32
33
  check_ide_settings: boolean;
33
34
  owasp_mapping: boolean;
34
35
  trusted_api_domains: string[];
36
+ rule_pack_paths?: string[];
37
+ allowed_rules?: string[];
38
+ skip_rules?: string[];
35
39
  suppress_findings: string[];
40
+ suppression_rules?: SuppressionRule[];
36
41
  }
37
42
  export interface CliConfigOverrides {
38
43
  format?: OutputFormat;
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
  import { parse as parseJsonc } from "jsonc-parser";
5
5
  import { applyReportSummary, computeExitCode as computeReportExitCode } from "./report-summary.js";
6
+ import { applySuppressionPolicy } from "./config/suppression-policy.js";
6
7
  export const OUTPUT_FORMATS = ["terminal", "json", "sarif", "markdown", "html"];
7
8
  export const SEVERITY_THRESHOLDS = ["critical", "high", "medium", "low", "info"];
8
9
  export const DEFAULT_CONFIG = {
@@ -34,7 +35,11 @@ export const DEFAULT_CONFIG = {
34
35
  check_ide_settings: true,
35
36
  owasp_mapping: true,
36
37
  trusted_api_domains: [],
38
+ rule_pack_paths: [],
39
+ allowed_rules: [],
40
+ skip_rules: [],
37
41
  suppress_findings: [],
42
+ suppression_rules: [],
38
43
  };
39
44
  function normalizeOutputFormat(value) {
40
45
  if (!value) {
@@ -163,22 +168,43 @@ export function resolveEffectiveConfig(options) {
163
168
  globalConfig.trusted_api_domains,
164
169
  projectConfig.trusted_api_domains,
165
170
  ]),
171
+ rule_pack_paths: unique([
172
+ DEFAULT_CONFIG.rule_pack_paths,
173
+ globalConfig.rule_pack_paths,
174
+ projectConfig.rule_pack_paths,
175
+ ]),
176
+ allowed_rules: unique([
177
+ DEFAULT_CONFIG.allowed_rules,
178
+ globalConfig.allowed_rules,
179
+ projectConfig.allowed_rules,
180
+ ]),
181
+ skip_rules: unique([
182
+ DEFAULT_CONFIG.skip_rules,
183
+ globalConfig.skip_rules,
184
+ projectConfig.skip_rules,
185
+ ]),
166
186
  suppress_findings: unique([
167
187
  DEFAULT_CONFIG.suppress_findings,
168
188
  globalConfig.suppress_findings,
169
189
  projectConfig.suppress_findings,
170
190
  ]),
191
+ suppression_rules: [
192
+ ...(DEFAULT_CONFIG.suppression_rules ?? []),
193
+ ...(globalConfig.suppression_rules ?? []),
194
+ ...(projectConfig.suppression_rules ?? []),
195
+ ],
171
196
  };
172
197
  }
173
198
  export function computeExitCode(findings, threshold) {
174
199
  return computeReportExitCode(findings, threshold);
175
200
  }
176
201
  export function applyConfigPolicy(report, config) {
177
- const suppressionSet = new Set(config.suppress_findings);
178
- const findings = report.findings.map((finding) => ({
202
+ const findings = applySuppressionPolicy(report.findings, {
203
+ suppress_findings: config.suppress_findings,
204
+ suppression_rules: config.suppression_rules,
205
+ }).map((finding) => ({
179
206
  ...finding,
180
207
  owasp: config.owasp_mapping ? finding.owasp : [],
181
- suppressed: finding.suppressed || suppressionSet.has(finding.finding_id),
182
208
  }));
183
209
  return applyReportSummary({
184
210
  ...report,
@@ -0,0 +1,62 @@
1
+ [
2
+ {
3
+ "id": "filesystem",
4
+ "rule_id": "advisory-agent-component-filesystem",
5
+ "severity": "MEDIUM",
6
+ "category": "CONFIG_PRESENT",
7
+ "description": "Filesystem agent components can expose broad local file access and should be reviewed before approval.",
8
+ "signatures": ["@anthropic/mcp-server-filesystem", "mcp-server-filesystem"],
9
+ "file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
10
+ "remediation_actions": ["remove_field", "review_component"],
11
+ "metadata": {
12
+ "sources": ["mcpServers.*.command", "mcpServers.*.args"],
13
+ "sinks": ["local_filesystem"],
14
+ "referenced_secrets": [],
15
+ "risk_tags": ["sensitive_access"],
16
+ "origin": "agent-components.json"
17
+ },
18
+ "owasp": ["ASI03", "ASI07"],
19
+ "cwe": "CWE-200",
20
+ "confidence": "HIGH"
21
+ },
22
+ {
23
+ "id": "github",
24
+ "rule_id": "advisory-agent-component-github",
25
+ "severity": "MEDIUM",
26
+ "category": "CONFIG_PRESENT",
27
+ "description": "GitHub agent components can ingest untrusted repository content and deserve manual review.",
28
+ "signatures": ["@modelcontextprotocol/server-github", "server-github"],
29
+ "file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
30
+ "remediation_actions": ["remove_field", "review_component"],
31
+ "metadata": {
32
+ "sources": ["mcpServers.*.command", "mcpServers.*.args"],
33
+ "sinks": ["repository_content"],
34
+ "referenced_secrets": [],
35
+ "risk_tags": ["untrusted_input"],
36
+ "origin": "agent-components.json"
37
+ },
38
+ "owasp": ["ASI01", "ASI07"],
39
+ "cwe": "CWE-74",
40
+ "confidence": "HIGH"
41
+ },
42
+ {
43
+ "id": "slack",
44
+ "rule_id": "advisory-agent-component-slack",
45
+ "severity": "HIGH",
46
+ "category": "CONFIG_PRESENT",
47
+ "description": "Slack agent components can become exfiltration sinks and should be approved deliberately.",
48
+ "signatures": ["@modelcontextprotocol/server-slack", "server-slack"],
49
+ "file_patterns": ["**/.mcp.json", "**/mcp.json", "**/settings.json"],
50
+ "remediation_actions": ["remove_field", "review_component"],
51
+ "metadata": {
52
+ "sources": ["mcpServers.*.command", "mcpServers.*.args"],
53
+ "sinks": ["message_exfiltration"],
54
+ "referenced_secrets": [],
55
+ "risk_tags": ["exfiltration_sink"],
56
+ "origin": "agent-components.json"
57
+ },
58
+ "owasp": ["ASI07", "ASI09"],
59
+ "cwe": "CWE-200",
60
+ "confidence": "HIGH"
61
+ }
62
+ ]
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface AdvisoryIntelligenceInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectAdvisoryIntelligence(input: AdvisoryIntelligenceInput): Finding[];
@@ -0,0 +1,170 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { buildFindingEvidence } from "../evidence.js";
5
+ const COMPONENTS_PATH = join(dirname(fileURLToPath(import.meta.url)), "../advisories/agent-components.json");
6
+ const GENERIC_AFFECTED_TOOLS = [
7
+ "claude-code",
8
+ "codex-cli",
9
+ "opencode",
10
+ "cursor",
11
+ "windsurf",
12
+ "github-copilot",
13
+ ];
14
+ function globToRegExp(glob) {
15
+ const pattern = glob.replaceAll("\\", "/").trim();
16
+ if (pattern.length === 0) {
17
+ return /^$/u;
18
+ }
19
+ let regex = "^";
20
+ for (let index = 0; index < pattern.length; index += 1) {
21
+ const char = pattern[index];
22
+ if (char === "*") {
23
+ if (pattern[index + 1] === "*") {
24
+ regex += ".*";
25
+ index += 1;
26
+ }
27
+ else {
28
+ regex += "[^/]*";
29
+ }
30
+ continue;
31
+ }
32
+ if (char === "?") {
33
+ regex += "[^/]";
34
+ continue;
35
+ }
36
+ if ("\\^$.*+?()[]{}|".includes(char)) {
37
+ regex += `\\${char}`;
38
+ continue;
39
+ }
40
+ regex += char;
41
+ }
42
+ regex += "$";
43
+ return new RegExp(regex, "u");
44
+ }
45
+ function matchesGlob(value, glob) {
46
+ const normalizedValue = value.replaceAll("\\", "/");
47
+ const normalizedGlob = glob.replaceAll("\\", "/").trim();
48
+ if (normalizedGlob.startsWith("**/")) {
49
+ const suffix = normalizedGlob.slice(3);
50
+ return normalizedValue === suffix || normalizedValue.endsWith(`/${suffix}`);
51
+ }
52
+ return globToRegExp(normalizedGlob).test(normalizedValue);
53
+ }
54
+ function loadComponents() {
55
+ const raw = readFileSync(COMPONENTS_PATH, "utf8");
56
+ const parsed = JSON.parse(raw);
57
+ return parsed;
58
+ }
59
+ const ADVISORY_COMPONENTS = loadComponents();
60
+ function normalizeToken(value) {
61
+ return value.trim().toLowerCase();
62
+ }
63
+ function collectStringCandidates(value, path = [], output = []) {
64
+ if (typeof value === "string") {
65
+ output.push({ path: path.join("."), value });
66
+ return output;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ value.forEach((entry, index) => {
70
+ collectStringCandidates(entry, [...path, String(index)], output);
71
+ });
72
+ return output;
73
+ }
74
+ if (!value || typeof value !== "object") {
75
+ return output;
76
+ }
77
+ for (const [key, child] of Object.entries(value)) {
78
+ collectStringCandidates(child, [...path, key], output);
79
+ }
80
+ return output;
81
+ }
82
+ function makeFinding(filePath, matchedPath, component, signature, evidence) {
83
+ const location = { field: matchedPath };
84
+ if (typeof evidence?.line === "number") {
85
+ location.line = evidence.line;
86
+ }
87
+ if (typeof evidence?.column === "number") {
88
+ location.column = evidence.column;
89
+ }
90
+ return {
91
+ rule_id: component.rule_id,
92
+ finding_id: `${component.rule_id}-${filePath}-${matchedPath}`,
93
+ severity: component.severity,
94
+ category: component.category,
95
+ layer: "L2",
96
+ file_path: filePath,
97
+ location,
98
+ description: component.description,
99
+ affected_tools: GENERIC_AFFECTED_TOOLS,
100
+ cve: null,
101
+ owasp: component.owasp,
102
+ cwe: component.cwe,
103
+ confidence: component.confidence,
104
+ fixable: true,
105
+ remediation_actions: component.remediation_actions,
106
+ metadata: {
107
+ sources: component.metadata?.sources ?? [],
108
+ sinks: component.metadata?.sinks ?? [],
109
+ referenced_secrets: component.metadata?.referenced_secrets ?? [],
110
+ risk_tags: component.metadata?.risk_tags ?? [],
111
+ origin: component.metadata?.origin ?? "agent-components.json",
112
+ },
113
+ evidence: evidence?.evidence ?? null,
114
+ suppressed: false,
115
+ };
116
+ }
117
+ function componentMatchesFile(component, filePath) {
118
+ return component.file_patterns.some((pattern) => matchesGlob(filePath, pattern));
119
+ }
120
+ function signatureMatches(value, signatures) {
121
+ const normalizedValue = normalizeToken(value);
122
+ for (const signature of signatures) {
123
+ const normalizedSignature = normalizeToken(signature);
124
+ if (normalizedSignature.length > 0 && normalizedValue.includes(normalizedSignature)) {
125
+ return signature;
126
+ }
127
+ }
128
+ return null;
129
+ }
130
+ function detectComponentMatch(input, component) {
131
+ if (!componentMatchesFile(component, input.filePath)) {
132
+ return null;
133
+ }
134
+ const stringCandidates = collectStringCandidates(input.parsed);
135
+ for (const candidate of stringCandidates) {
136
+ const signature = signatureMatches(candidate.value, component.signatures);
137
+ if (!signature) {
138
+ continue;
139
+ }
140
+ const evidence = buildFindingEvidence({
141
+ textContent: input.textContent,
142
+ jsonPaths: candidate.path.length > 0 ? [candidate.path] : [],
143
+ searchTerms: [signature],
144
+ fallbackValue: `${candidate.path} = ${candidate.value}`,
145
+ });
146
+ return makeFinding(input.filePath, candidate.path || component.id, component, signature, evidence);
147
+ }
148
+ for (const signature of component.signatures) {
149
+ if (!signatureMatches(input.textContent, [signature])) {
150
+ continue;
151
+ }
152
+ const evidence = buildFindingEvidence({
153
+ textContent: input.textContent,
154
+ searchTerms: [signature],
155
+ fallbackValue: signature,
156
+ });
157
+ return makeFinding(input.filePath, component.id, component, signature, evidence);
158
+ }
159
+ return null;
160
+ }
161
+ export function detectAdvisoryIntelligence(input) {
162
+ const findings = [];
163
+ for (const component of ADVISORY_COMPONENTS) {
164
+ const finding = detectComponentMatch(input, component);
165
+ if (finding) {
166
+ findings.push(finding);
167
+ }
168
+ }
169
+ return findings;
170
+ }
@@ -95,6 +95,12 @@ function makeFinding(filePath, field, ruleId, severity, description, evidence) {
95
95
  confidence: "HIGH",
96
96
  fixable: true,
97
97
  remediation_actions: ["remove_field", "replace_with_default"],
98
+ metadata: {
99
+ sources: [filePath, field],
100
+ sinks: ["process-execution"],
101
+ risk_tags: ["command-execution", "shell-pipeline"],
102
+ origin: "command-exec",
103
+ },
98
104
  evidence: evidence?.evidence ?? null,
99
105
  suppressed: false,
100
106
  };
@@ -48,6 +48,11 @@ function makeFinding(filePath, field, ruleId, description, evidence, severity =
48
48
  confidence: "HIGH",
49
49
  fixable: true,
50
50
  remediation_actions: ["strip_unicode", "remove_block", "quarantine_file"],
51
+ metadata: {
52
+ sources: [filePath, field],
53
+ risk_tags: ["rule-injection", "prompt-injection"],
54
+ origin: "rule-file",
55
+ },
51
56
  evidence: evidence?.evidence ?? null,
52
57
  observed: narrative.observed ?? null,
53
58
  inference: narrative.inference ?? null,
@@ -1,6 +1,6 @@
1
1
  import { type GitHookEntry } from "./detectors/git-hooks.js";
2
2
  import { type SymlinkEscapeEntry } from "./detectors/symlink.js";
3
- import type { Finding } from "../types/finding.js";
3
+ import { type Finding } from "../types/finding.js";
4
4
  import type { DiscoveryFormat } from "../types/discovery.js";
5
5
  export interface StaticFileInput {
6
6
  filePath: string;
@@ -17,6 +17,9 @@ export interface StaticEngineConfig {
17
17
  trustedApiDomains: string[];
18
18
  unicodeAnalysis: boolean;
19
19
  checkIdeSettings: boolean;
20
+ rulePackPaths?: string[];
21
+ allowedRules?: string[];
22
+ skipRules?: string[];
20
23
  }
21
24
  export interface StaticEngineInput {
22
25
  projectRoot: string;
@@ -1,4 +1,5 @@
1
1
  import { detectCommandExecution } from "./detectors/command-exec.js";
2
+ import { detectAdvisoryIntelligence } from "./detectors/advisory-intelligence.js";
2
3
  import { detectConsentBypass } from "./detectors/consent-bypass.js";
3
4
  import { detectEnvOverrides } from "./detectors/env-override.js";
4
5
  import { detectGitHookIssues } from "./detectors/git-hooks.js";
@@ -6,6 +7,78 @@ import { detectIdeSettingsIssues } from "./detectors/ide-settings.js";
6
7
  import { detectPluginManifestIssues } from "./detectors/plugin-manifest.js";
7
8
  import { detectRuleFileIssues } from "./detectors/rule-file.js";
8
9
  import { detectSymlinkEscapes } from "./detectors/symlink.js";
10
+ import { FINDING_CATEGORIES } from "../types/finding.js";
11
+ import { buildFindingEvidence } from "./evidence.js";
12
+ import { evaluateRule, loadRulePacks } from "./rule-engine.js";
13
+ const GENERIC_AFFECTED_TOOLS = [
14
+ "claude-code",
15
+ "codex-cli",
16
+ "opencode",
17
+ "cursor",
18
+ "windsurf",
19
+ "github-copilot",
20
+ ];
21
+ function parseRuleSeverity(value) {
22
+ const normalized = value.trim().toUpperCase();
23
+ if (normalized === "CRITICAL" ||
24
+ normalized === "HIGH" ||
25
+ normalized === "MEDIUM" ||
26
+ normalized === "LOW") {
27
+ return normalized;
28
+ }
29
+ return "INFO";
30
+ }
31
+ function parseRuleCategory(value) {
32
+ const normalized = value.trim().toUpperCase();
33
+ if (FINDING_CATEGORIES.some((category) => category === normalized)) {
34
+ return normalized;
35
+ }
36
+ return "CONFIG_PRESENT";
37
+ }
38
+ function remediationActionsForRule(rule) {
39
+ if (rule.query_type === "text_pattern") {
40
+ return ["quarantine_file", "remove_block"];
41
+ }
42
+ return ["remove_field", "replace_with_default"];
43
+ }
44
+ function findingFromRulePackMatch(file, rule) {
45
+ const locationField = rule.query_type === "text_pattern" ? "content" : rule.query;
46
+ const evidence = buildFindingEvidence({
47
+ textContent: file.textContent,
48
+ searchTerms: [rule.query],
49
+ fallbackValue: `${locationField} matched rule ${rule.id}`,
50
+ });
51
+ const affectedTools = rule.tool === "*" ? GENERIC_AFFECTED_TOOLS : [rule.tool];
52
+ return {
53
+ rule_id: rule.id,
54
+ finding_id: `RULE_PACK-${rule.id}-${file.filePath}-${locationField}`,
55
+ severity: parseRuleSeverity(rule.severity),
56
+ category: parseRuleCategory(rule.category),
57
+ layer: "L2",
58
+ file_path: file.filePath,
59
+ location: { field: locationField },
60
+ description: rule.description,
61
+ affected_tools: affectedTools,
62
+ cve: rule.cve ?? null,
63
+ owasp: rule.owasp,
64
+ cwe: rule.cwe,
65
+ confidence: "HIGH",
66
+ fixable: true,
67
+ remediation_actions: remediationActionsForRule(rule),
68
+ metadata: {
69
+ sources: [file.filePath, locationField],
70
+ risk_tags: ["rule-pack"],
71
+ origin: "rule-pack",
72
+ },
73
+ evidence: evidence?.evidence ?? null,
74
+ suppressed: false,
75
+ };
76
+ }
77
+ function hasEquivalentFinding(findings, candidate) {
78
+ return findings.some((finding) => finding.rule_id === candidate.rule_id &&
79
+ finding.file_path === candidate.file_path &&
80
+ (finding.location.field ?? "") === (candidate.location.field ?? ""));
81
+ }
9
82
  function dedupeFindings(findings) {
10
83
  const deduped = new Map();
11
84
  for (const finding of findings) {
@@ -32,6 +105,11 @@ function dedupeFindings(findings) {
32
105
  }
33
106
  export function runStaticEngine(input) {
34
107
  const findings = [];
108
+ const rulePackRules = loadRulePacks({
109
+ rule_pack_paths: input.config.rulePackPaths ?? [],
110
+ allowed_rules: input.config.allowedRules ?? [],
111
+ skip_rules: input.config.skipRules ?? [],
112
+ });
35
113
  for (const file of input.files) {
36
114
  findings.push(...detectEnvOverrides({
37
115
  filePath: file.filePath,
@@ -69,6 +147,11 @@ export function runStaticEngine(input) {
69
147
  trustedApiDomains: input.config.trustedApiDomains,
70
148
  blockedCommands: input.config.blockedCommands,
71
149
  }));
150
+ findings.push(...detectAdvisoryIntelligence({
151
+ filePath: file.filePath,
152
+ parsed: file.parsed,
153
+ textContent: file.textContent,
154
+ }));
72
155
  if (file.format === "text" || file.format === "markdown") {
73
156
  findings.push(...detectRuleFileIssues({
74
157
  filePath: file.filePath,
@@ -76,6 +159,20 @@ export function runStaticEngine(input) {
76
159
  unicodeAnalysis: input.config.unicodeAnalysis,
77
160
  }));
78
161
  }
162
+ for (const rule of rulePackRules) {
163
+ if (!evaluateRule(rule, {
164
+ filePath: file.filePath,
165
+ format: file.format,
166
+ parsed: file.parsed,
167
+ textContent: file.textContent,
168
+ })) {
169
+ continue;
170
+ }
171
+ const candidate = findingFromRulePackMatch(file, rule);
172
+ if (!hasEquivalentFinding(findings, candidate)) {
173
+ findings.push(candidate);
174
+ }
175
+ }
79
176
  }
80
177
  findings.push(...detectSymlinkEscapes({ symlinkEscapes: input.symlinkEscapes }));
81
178
  findings.push(...detectGitHookIssues({ hooks: input.hooks, knownSafeHooks: input.config.knownSafeHooks }));
@@ -21,4 +21,4 @@ export interface RuleEvaluationInput {
21
21
  textContent: string;
22
22
  }
23
23
  export declare function evaluateRule(rule: DetectionRule, input: RuleEvaluationInput): boolean;
24
- export declare function loadRulePacks(baseDir?: string): DetectionRule[];
24
+ export { loadRulePacks } from "./rule-pack-loader.js";
@@ -1,7 +1,3 @@
1
- import { readdirSync, readFileSync } from "node:fs";
2
- import { dirname, extname, join, resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- const rulesDir = resolve(dirname(fileURLToPath(import.meta.url)), "rules");
5
1
  function escapeRegex(value) {
6
2
  return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
7
3
  }
@@ -127,12 +123,4 @@ export function evaluateRule(rule, input) {
127
123
  }
128
124
  return false;
129
125
  }
130
- export function loadRulePacks(baseDir = rulesDir) {
131
- const files = readdirSync(baseDir)
132
- .filter((file) => extname(file) === ".json")
133
- .sort();
134
- return files.flatMap((file) => {
135
- const raw = readFileSync(join(baseDir, file), "utf8");
136
- return JSON.parse(raw);
137
- });
138
- }
126
+ export { loadRulePacks } from "./rule-pack-loader.js";
@@ -0,0 +1,10 @@
1
+ import type { DetectionRule } from "./rule-engine.js";
2
+ export interface RulePackLoaderOptions {
3
+ baseDir?: string;
4
+ rule_pack_paths?: string[];
5
+ allowed_rules?: string[];
6
+ skip_rules?: string[];
7
+ }
8
+ export declare function loadRulePacks(): DetectionRule[];
9
+ export declare function loadRulePacks(baseDir: string): DetectionRule[];
10
+ export declare function loadRulePacks(options: RulePackLoaderOptions): DetectionRule[];