codegate-ai 0.7.0 → 0.9.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 (135) hide show
  1. package/README.md +109 -40
  2. package/dist/cli.js +85 -7
  3. package/dist/commands/scan-command.d.ts +6 -1
  4. package/dist/commands/scan-command.js +1 -1
  5. package/dist/commands/scan-content-command.d.ts +16 -0
  6. package/dist/commands/scan-content-command.js +65 -0
  7. package/dist/config/inline-ignore.d.ts +11 -0
  8. package/dist/config/inline-ignore.js +57 -0
  9. package/dist/config/suppression-policy.d.ts +22 -0
  10. package/dist/config/suppression-policy.js +146 -0
  11. package/dist/config.d.ts +23 -0
  12. package/dist/config.js +188 -3
  13. package/dist/layer2-static/action/parser.d.ts +3 -0
  14. package/dist/layer2-static/action/parser.js +165 -0
  15. package/dist/layer2-static/action/types.d.ts +39 -0
  16. package/dist/layer2-static/action/types.js +1 -0
  17. package/dist/layer2-static/advisories/agent-components.json +62 -0
  18. package/dist/layer2-static/advisories/cache.d.ts +6 -0
  19. package/dist/layer2-static/advisories/cache.js +44 -0
  20. package/dist/layer2-static/advisories/cache.ts +62 -0
  21. package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
  22. package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
  23. package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
  24. package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
  25. package/dist/layer2-static/audits/registry.d.ts +3 -0
  26. package/dist/layer2-static/audits/registry.js +29 -0
  27. package/dist/layer2-static/audits/types.d.ts +12 -0
  28. package/dist/layer2-static/audits/types.js +1 -0
  29. package/dist/layer2-static/dependabot/parser.d.ts +3 -0
  30. package/dist/layer2-static/dependabot/parser.js +189 -0
  31. package/dist/layer2-static/dependabot/types.d.ts +50 -0
  32. package/dist/layer2-static/dependabot/types.js +1 -0
  33. package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
  34. package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
  35. package/dist/layer2-static/detectors/command-exec.js +6 -0
  36. package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
  37. package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
  38. package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
  39. package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
  40. package/dist/layer2-static/detectors/rule-file.js +5 -0
  41. package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
  42. package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
  43. package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
  44. package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
  45. package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
  46. package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
  47. package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
  48. package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
  49. package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
  50. package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
  51. package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
  52. package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
  53. package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
  54. package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
  55. package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
  56. package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
  57. package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
  58. package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
  59. package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
  60. package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
  61. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
  62. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
  63. package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
  64. package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
  65. package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
  66. package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
  67. package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
  68. package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
  69. package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
  70. package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
  71. package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
  72. package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
  73. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
  74. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
  75. package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
  76. package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
  77. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
  78. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
  79. package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
  80. package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
  81. package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
  82. package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
  83. package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
  84. package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
  85. package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
  86. package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
  87. package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
  88. package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
  89. package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
  90. package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
  91. package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
  92. package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
  93. package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
  94. package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
  95. package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
  96. package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
  97. package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
  98. package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
  99. package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
  100. package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
  101. package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
  102. package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
  103. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
  104. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
  105. package/dist/layer2-static/engine.d.ts +13 -2
  106. package/dist/layer2-static/engine.js +578 -41
  107. package/dist/layer2-static/github/cache.d.ts +8 -0
  108. package/dist/layer2-static/github/cache.js +41 -0
  109. package/dist/layer2-static/github/client.d.ts +17 -0
  110. package/dist/layer2-static/github/client.js +43 -0
  111. package/dist/layer2-static/rule-engine.d.ts +1 -1
  112. package/dist/layer2-static/rule-engine.js +1 -13
  113. package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
  114. package/dist/layer2-static/rule-pack-loader.js +187 -0
  115. package/dist/layer2-static/workflow/injection-sinks.json +5 -0
  116. package/dist/layer2-static/workflow/parser.d.ts +3 -0
  117. package/dist/layer2-static/workflow/parser.js +88 -0
  118. package/dist/layer2-static/workflow/types.d.ts +15 -0
  119. package/dist/layer2-static/workflow/types.js +1 -0
  120. package/dist/layer3-dynamic/toxic-flow.js +6 -0
  121. package/dist/pipeline.d.ts +1 -1
  122. package/dist/pipeline.js +11 -10
  123. package/dist/report/finding-fingerprint.d.ts +5 -0
  124. package/dist/report/finding-fingerprint.js +47 -0
  125. package/dist/reporter/markdown.js +25 -3
  126. package/dist/reporter/sarif.js +25 -5
  127. package/dist/reporter/terminal.js +37 -0
  128. package/dist/scan-target/fetch-plan.d.ts +8 -0
  129. package/dist/scan-target/fetch-plan.js +30 -0
  130. package/dist/scan-target/staging.js +60 -5
  131. package/dist/scan.d.ts +7 -1
  132. package/dist/scan.js +157 -40
  133. package/dist/types/finding.d.ts +10 -1
  134. package/dist/types/finding.js +5 -0
  135. package/package.json +1 -1
@@ -0,0 +1,146 @@
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 parseSuppressionLocation(value) {
42
+ const trimmed = normalizeString(value);
43
+ if (!trimmed) {
44
+ return null;
45
+ }
46
+ const pieces = trimmed.split(":");
47
+ if (pieces.length === 0 || pieces.length > 3) {
48
+ return null;
49
+ }
50
+ const [filePath, lineRaw, columnRaw] = pieces;
51
+ if (!filePath) {
52
+ return null;
53
+ }
54
+ const line = lineRaw !== undefined ? Number.parseInt(lineRaw, 10) : undefined;
55
+ const column = columnRaw !== undefined ? Number.parseInt(columnRaw, 10) : undefined;
56
+ if (lineRaw !== undefined && (!Number.isFinite(line) || (line ?? 0) < 1)) {
57
+ return null;
58
+ }
59
+ if (columnRaw !== undefined && (!Number.isFinite(column) || (column ?? 0) < 1)) {
60
+ return null;
61
+ }
62
+ return {
63
+ filePath: filePath.replaceAll("\\", "/"),
64
+ line,
65
+ column,
66
+ };
67
+ }
68
+ function matchesSuppressionRule(finding, rule) {
69
+ const ruleId = normalizeString(rule.rule_id);
70
+ if (ruleId && finding.rule_id !== ruleId) {
71
+ return false;
72
+ }
73
+ const filePath = normalizeString(rule.file_path);
74
+ if (filePath && !matchesGlob(finding.file_path, filePath)) {
75
+ return false;
76
+ }
77
+ const location = parseSuppressionLocation(rule.location ?? "");
78
+ if (location) {
79
+ const normalizedFindingPath = finding.file_path.replaceAll("\\", "/");
80
+ if (normalizedFindingPath !== location.filePath) {
81
+ return false;
82
+ }
83
+ if (typeof location.line === "number" &&
84
+ (typeof finding.location.line !== "number" || finding.location.line !== location.line)) {
85
+ return false;
86
+ }
87
+ if (typeof location.column === "number" &&
88
+ (typeof finding.location.column !== "number" || finding.location.column !== location.column)) {
89
+ return false;
90
+ }
91
+ }
92
+ const severity = rule.severity;
93
+ if (severity && finding.severity !== severity) {
94
+ return false;
95
+ }
96
+ const category = normalizeString(rule.category);
97
+ if (category && finding.category !== category) {
98
+ return false;
99
+ }
100
+ const cwe = normalizeString(rule.cwe);
101
+ if (cwe && finding.cwe !== cwe) {
102
+ return false;
103
+ }
104
+ const fingerprint = normalizeString(rule.fingerprint);
105
+ if (fingerprint && finding.fingerprint !== fingerprint) {
106
+ return false;
107
+ }
108
+ return true;
109
+ }
110
+ function matchesRulePolicyIgnore(finding, location) {
111
+ const parsed = parseSuppressionLocation(location);
112
+ if (!parsed) {
113
+ return false;
114
+ }
115
+ const normalizedFindingPath = finding.file_path.replaceAll("\\", "/");
116
+ if (normalizedFindingPath !== parsed.filePath) {
117
+ return false;
118
+ }
119
+ if (typeof parsed.line === "number" &&
120
+ (typeof finding.location.line !== "number" || finding.location.line !== parsed.line)) {
121
+ return false;
122
+ }
123
+ if (typeof parsed.column === "number" &&
124
+ (typeof finding.location.column !== "number" || finding.location.column !== parsed.column)) {
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+ export function applySuppressionPolicy(findings, policy) {
130
+ const legacySuppressions = new Set((policy.suppress_findings ?? [])
131
+ .map((findingId) => normalizeString(findingId))
132
+ .filter((findingId) => findingId !== undefined));
133
+ const rules = policy.suppression_rules ?? [];
134
+ const rulePolicies = policy.rule_policies ?? {};
135
+ return findings.map((finding) => {
136
+ const ruleMatch = rules.some((rule) => matchesSuppressionRule(finding, rule));
137
+ const legacyMatch = legacySuppressions.has(finding.finding_id);
138
+ const rulePolicy = rulePolicies[finding.rule_id];
139
+ const ruleDisabled = rulePolicy?.disable === true;
140
+ const ruleIgnoreMatch = rulePolicy?.ignore?.some((location) => matchesRulePolicyIgnore(finding, location)) ?? false;
141
+ return {
142
+ ...finding,
143
+ suppressed: finding.suppressed || legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch,
144
+ };
145
+ });
146
+ }
package/dist/config.d.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  import type { Finding } from "./types/finding.js";
2
2
  import type { CodeGateReport } from "./types/report.js";
3
+ import { type RulePolicyMap, 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];
6
+ export declare const SCAN_COLLECTION_MODES: readonly ["default", "project", "user", "explicit", "all"];
7
+ export type ScanCollectionMode = (typeof SCAN_COLLECTION_MODES)[number];
8
+ export declare const SCAN_COLLECTION_KINDS: readonly ["workflows", "actions", "dependabot"];
9
+ export type ScanCollectionKind = (typeof SCAN_COLLECTION_KINDS)[number];
10
+ export declare const PERSONAS: readonly ["regular", "pedantic", "auditor"];
11
+ export type AuditPersona = (typeof PERSONAS)[number];
12
+ export declare const RUNTIME_MODES: readonly ["offline", "online", "online-no-audits"];
13
+ export type RuntimeMode = (typeof RUNTIME_MODES)[number];
5
14
  export declare const SEVERITY_THRESHOLDS: readonly ["critical", "high", "medium", "low", "info"];
6
15
  export type SeverityThreshold = (typeof SEVERITY_THRESHOLDS)[number];
7
16
  export interface TuiConfig {
@@ -14,6 +23,9 @@ export interface ToolDiscoveryConfig {
14
23
  agent_paths: Record<string, string>;
15
24
  skip_tools: string[];
16
25
  }
26
+ export interface WorkflowAuditConfig {
27
+ enabled: boolean;
28
+ }
17
29
  export interface CodeGateConfig {
18
30
  severity_threshold: SeverityThreshold;
19
31
  auto_proceed_below_threshold: boolean;
@@ -32,7 +44,18 @@ export interface CodeGateConfig {
32
44
  check_ide_settings: boolean;
33
45
  owasp_mapping: boolean;
34
46
  trusted_api_domains: string[];
47
+ rule_pack_paths?: string[];
48
+ allowed_rules?: string[];
49
+ skip_rules?: string[];
50
+ strict_collection?: boolean;
51
+ scan_collection_modes?: ScanCollectionMode[];
52
+ scan_collection_kinds?: ScanCollectionKind[];
53
+ rules?: RulePolicyMap;
54
+ persona?: AuditPersona;
55
+ runtime_mode?: RuntimeMode;
56
+ workflow_audits?: WorkflowAuditConfig;
35
57
  suppress_findings: string[];
58
+ suppression_rules?: SuppressionRule[];
36
59
  }
37
60
  export interface CliConfigOverrides {
38
61
  format?: OutputFormat;
package/dist/config.js CHANGED
@@ -3,7 +3,12 @@ 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"];
8
+ export const SCAN_COLLECTION_MODES = ["default", "project", "user", "explicit", "all"];
9
+ export const SCAN_COLLECTION_KINDS = ["workflows", "actions", "dependabot"];
10
+ export const PERSONAS = ["regular", "pedantic", "auditor"];
11
+ export const RUNTIME_MODES = ["offline", "online", "online-no-audits"];
7
12
  export const SEVERITY_THRESHOLDS = ["critical", "high", "medium", "low", "info"];
8
13
  export const DEFAULT_CONFIG = {
9
14
  severity_threshold: "high",
@@ -34,7 +39,16 @@ export const DEFAULT_CONFIG = {
34
39
  check_ide_settings: true,
35
40
  owasp_mapping: true,
36
41
  trusted_api_domains: [],
42
+ rule_pack_paths: [],
43
+ allowed_rules: [],
44
+ skip_rules: [],
45
+ strict_collection: false,
46
+ scan_collection_modes: ["default"],
47
+ persona: "regular",
48
+ runtime_mode: "offline",
49
+ workflow_audits: { enabled: false },
37
50
  suppress_findings: [],
51
+ suppression_rules: [],
38
52
  };
39
53
  function normalizeOutputFormat(value) {
40
54
  if (!value) {
@@ -55,6 +69,146 @@ function normalizeOptionalPath(value) {
55
69
  const trimmed = value.trim();
56
70
  return trimmed.length > 0 ? trimmed : undefined;
57
71
  }
72
+ function normalizeCollectionMode(value) {
73
+ if (!value) {
74
+ return undefined;
75
+ }
76
+ return SCAN_COLLECTION_MODES.find((mode) => mode === value) ?? undefined;
77
+ }
78
+ function normalizeCollectionModes(values) {
79
+ if (!values) {
80
+ return undefined;
81
+ }
82
+ const normalized = [];
83
+ for (const value of values) {
84
+ const mode = normalizeCollectionMode(value);
85
+ if (!mode || normalized.includes(mode)) {
86
+ continue;
87
+ }
88
+ normalized.push(mode);
89
+ }
90
+ return normalized.length > 0 ? normalized : undefined;
91
+ }
92
+ function normalizeCollectionKind(value) {
93
+ if (!value) {
94
+ return undefined;
95
+ }
96
+ return SCAN_COLLECTION_KINDS.find((kind) => kind === value) ?? undefined;
97
+ }
98
+ function normalizeCollectionKinds(values) {
99
+ if (!values) {
100
+ return undefined;
101
+ }
102
+ const normalized = [];
103
+ for (const value of values) {
104
+ const kind = normalizeCollectionKind(value);
105
+ if (!kind || normalized.includes(kind)) {
106
+ continue;
107
+ }
108
+ normalized.push(kind);
109
+ }
110
+ return normalized.length > 0 ? normalized : undefined;
111
+ }
112
+ function isPlainObject(value) {
113
+ return !!value && typeof value === "object" && !Array.isArray(value);
114
+ }
115
+ function normalizeRulePolicyConfig(value) {
116
+ if (!isPlainObject(value)) {
117
+ return undefined;
118
+ }
119
+ const disable = typeof value.disable === "boolean" ? value.disable : undefined;
120
+ const ignoreRaw = value.ignore;
121
+ const ignore = Array.isArray(ignoreRaw) && ignoreRaw.length > 0
122
+ ? ignoreRaw
123
+ .filter((entry) => typeof entry === "string")
124
+ .map((entry) => entry.trim())
125
+ .filter((entry) => entry.length > 0)
126
+ : undefined;
127
+ const configRaw = value.config;
128
+ const config = isPlainObject(configRaw) ? { ...configRaw } : undefined;
129
+ if (disable === undefined && ignore === undefined && config === undefined) {
130
+ return undefined;
131
+ }
132
+ return {
133
+ disable,
134
+ ignore,
135
+ config,
136
+ };
137
+ }
138
+ function normalizeRulePolicyMap(rules) {
139
+ if (!rules || typeof rules !== "object") {
140
+ return undefined;
141
+ }
142
+ const normalized = {};
143
+ for (const [ruleId, value] of Object.entries(rules)) {
144
+ const key = ruleId.trim();
145
+ if (key.length === 0) {
146
+ continue;
147
+ }
148
+ const policy = normalizeRulePolicyConfig(value);
149
+ if (!policy) {
150
+ continue;
151
+ }
152
+ normalized[key] = policy;
153
+ }
154
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
155
+ }
156
+ function mergeRulePolicyConfig(current, incoming) {
157
+ if (!current) {
158
+ if (!incoming) {
159
+ return undefined;
160
+ }
161
+ return {
162
+ disable: incoming.disable,
163
+ ignore: incoming.ignore ? [...incoming.ignore] : undefined,
164
+ config: incoming.config ? { ...incoming.config } : undefined,
165
+ };
166
+ }
167
+ if (!incoming) {
168
+ return {
169
+ disable: current.disable,
170
+ ignore: current.ignore ? [...current.ignore] : undefined,
171
+ config: current.config ? { ...current.config } : undefined,
172
+ };
173
+ }
174
+ return {
175
+ disable: incoming.disable ?? current.disable,
176
+ ignore: unique([
177
+ current.ignore,
178
+ incoming.ignore,
179
+ ]),
180
+ config: current.config || incoming.config
181
+ ? {
182
+ ...(current.config ?? {}),
183
+ ...(incoming.config ?? {}),
184
+ }
185
+ : undefined,
186
+ };
187
+ }
188
+ function mergeRulePolicyMaps(...maps) {
189
+ const merged = {};
190
+ for (const map of maps) {
191
+ if (!map) {
192
+ continue;
193
+ }
194
+ for (const [ruleId, policy] of Object.entries(map)) {
195
+ merged[ruleId] = mergeRulePolicyConfig(merged[ruleId], policy) ?? merged[ruleId];
196
+ }
197
+ }
198
+ return Object.keys(merged).length > 0 ? merged : undefined;
199
+ }
200
+ function normalizePersona(value) {
201
+ if (!value) {
202
+ return undefined;
203
+ }
204
+ return PERSONAS.find((persona) => persona === value) ?? undefined;
205
+ }
206
+ function normalizeRuntimeMode(value) {
207
+ if (!value) {
208
+ return undefined;
209
+ }
210
+ return RUNTIME_MODES.find((mode) => mode === value) ?? undefined;
211
+ }
58
212
  function unique(values) {
59
213
  const merged = values.flatMap((entry) => entry ?? []);
60
214
  const seen = new Set();
@@ -163,22 +317,53 @@ export function resolveEffectiveConfig(options) {
163
317
  globalConfig.trusted_api_domains,
164
318
  projectConfig.trusted_api_domains,
165
319
  ]),
320
+ rule_pack_paths: unique([
321
+ DEFAULT_CONFIG.rule_pack_paths,
322
+ globalConfig.rule_pack_paths,
323
+ projectConfig.rule_pack_paths,
324
+ ]),
325
+ allowed_rules: unique([
326
+ DEFAULT_CONFIG.allowed_rules,
327
+ globalConfig.allowed_rules,
328
+ projectConfig.allowed_rules,
329
+ ]),
330
+ skip_rules: unique([
331
+ DEFAULT_CONFIG.skip_rules,
332
+ globalConfig.skip_rules,
333
+ projectConfig.skip_rules,
334
+ ]),
335
+ strict_collection: pickFirst(projectConfig.strict_collection, globalConfig.strict_collection, DEFAULT_CONFIG.strict_collection) ?? DEFAULT_CONFIG.strict_collection,
336
+ scan_collection_modes: pickFirst(normalizeCollectionModes(projectConfig.scan_collection_modes), normalizeCollectionModes(globalConfig.scan_collection_modes), DEFAULT_CONFIG.scan_collection_modes) ?? DEFAULT_CONFIG.scan_collection_modes,
337
+ scan_collection_kinds: pickFirst(normalizeCollectionKinds(projectConfig.scan_collection_kinds), normalizeCollectionKinds(globalConfig.scan_collection_kinds)) ?? undefined,
338
+ rules: mergeRulePolicyMaps(normalizeRulePolicyMap(globalConfig.rules), normalizeRulePolicyMap(projectConfig.rules)) ?? undefined,
339
+ persona: pickFirst(normalizePersona(projectConfig.persona), normalizePersona(globalConfig.persona), DEFAULT_CONFIG.persona) ?? DEFAULT_CONFIG.persona,
340
+ runtime_mode: pickFirst(normalizeRuntimeMode(projectConfig.runtime_mode), normalizeRuntimeMode(globalConfig.runtime_mode), DEFAULT_CONFIG.runtime_mode) ?? DEFAULT_CONFIG.runtime_mode,
341
+ workflow_audits: {
342
+ enabled: pickFirst(projectConfig.workflow_audits?.enabled, globalConfig.workflow_audits?.enabled, DEFAULT_CONFIG.workflow_audits?.enabled) ?? false,
343
+ },
166
344
  suppress_findings: unique([
167
345
  DEFAULT_CONFIG.suppress_findings,
168
346
  globalConfig.suppress_findings,
169
347
  projectConfig.suppress_findings,
170
348
  ]),
349
+ suppression_rules: [
350
+ ...(DEFAULT_CONFIG.suppression_rules ?? []),
351
+ ...(globalConfig.suppression_rules ?? []),
352
+ ...(projectConfig.suppression_rules ?? []),
353
+ ],
171
354
  };
172
355
  }
173
356
  export function computeExitCode(findings, threshold) {
174
357
  return computeReportExitCode(findings, threshold);
175
358
  }
176
359
  export function applyConfigPolicy(report, config) {
177
- const suppressionSet = new Set(config.suppress_findings);
178
- const findings = report.findings.map((finding) => ({
360
+ const findings = applySuppressionPolicy(report.findings, {
361
+ suppress_findings: config.suppress_findings,
362
+ suppression_rules: config.suppression_rules,
363
+ rule_policies: config.rules,
364
+ }).map((finding) => ({
179
365
  ...finding,
180
366
  owasp: config.owasp_mapping ? finding.owasp : [],
181
- suppressed: finding.suppressed || suppressionSet.has(finding.finding_id),
182
367
  }));
183
368
  return applyReportSummary({
184
369
  ...report,
@@ -0,0 +1,3 @@
1
+ import type { ActionFacts } from "./types.js";
2
+ export declare function isGitHubActionPath(path: string): boolean;
3
+ export declare function extractActionFacts(parsed: unknown): ActionFacts | null;
@@ -0,0 +1,165 @@
1
+ function asRecord(value) {
2
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3
+ return null;
4
+ }
5
+ return value;
6
+ }
7
+ function asString(value) {
8
+ return typeof value === "string" ? value : undefined;
9
+ }
10
+ function asBoolean(value) {
11
+ return typeof value === "boolean" ? value : undefined;
12
+ }
13
+ function asStringRecord(value) {
14
+ const record = asRecord(value);
15
+ if (!record) {
16
+ return undefined;
17
+ }
18
+ const result = {};
19
+ for (const [key, entry] of Object.entries(record)) {
20
+ if (typeof entry === "string") {
21
+ result[key] = entry;
22
+ }
23
+ }
24
+ return Object.keys(result).length > 0 ? result : undefined;
25
+ }
26
+ function asStringArray(value) {
27
+ if (!Array.isArray(value)) {
28
+ return undefined;
29
+ }
30
+ const result = value.filter((entry) => typeof entry === "string");
31
+ return result.length > 0 ? result : undefined;
32
+ }
33
+ function normalizeActionPath(value) {
34
+ return value.replaceAll("\\", "/");
35
+ }
36
+ export function isGitHubActionPath(path) {
37
+ return /(?:^|\/)action\.ya?ml$/iu.test(normalizeActionPath(path));
38
+ }
39
+ function extractStepFacts(step) {
40
+ const stepRecord = asRecord(step);
41
+ if (!stepRecord) {
42
+ return null;
43
+ }
44
+ const withEntries = asStringRecord(stepRecord.with);
45
+ const envEntries = asStringRecord(stepRecord.env);
46
+ const stepFacts = {
47
+ id: asString(stepRecord.id),
48
+ name: asString(stepRecord.name),
49
+ uses: asString(stepRecord.uses),
50
+ run: asString(stepRecord.run),
51
+ if: asString(stepRecord.if),
52
+ shell: asString(stepRecord.shell),
53
+ workingDirectory: asString(stepRecord["working-directory"]) ?? asString(stepRecord.workingDirectory),
54
+ with: withEntries,
55
+ env: envEntries,
56
+ };
57
+ if (!stepFacts.uses && !stepFacts.run) {
58
+ return null;
59
+ }
60
+ return stepFacts;
61
+ }
62
+ function extractInputs(value) {
63
+ const inputs = asRecord(value);
64
+ if (!inputs) {
65
+ return undefined;
66
+ }
67
+ const result = {};
68
+ for (const [name, inputValue] of Object.entries(inputs)) {
69
+ const inputRecord = asRecord(inputValue);
70
+ if (!inputRecord) {
71
+ continue;
72
+ }
73
+ const entry = {
74
+ description: asString(inputRecord.description),
75
+ required: asBoolean(inputRecord.required),
76
+ default: asString(inputRecord.default),
77
+ deprecationMessage: asString(inputRecord.deprecationMessage) ?? asString(inputRecord.deprecation_message),
78
+ };
79
+ if (entry.description !== undefined ||
80
+ entry.required !== undefined ||
81
+ entry.default !== undefined ||
82
+ entry.deprecationMessage !== undefined) {
83
+ result[name] = entry;
84
+ }
85
+ }
86
+ return Object.keys(result).length > 0 ? result : undefined;
87
+ }
88
+ function extractOutputs(value) {
89
+ const outputs = asRecord(value);
90
+ if (!outputs) {
91
+ return undefined;
92
+ }
93
+ const result = {};
94
+ for (const [name, outputValue] of Object.entries(outputs)) {
95
+ const outputRecord = asRecord(outputValue);
96
+ if (!outputRecord) {
97
+ continue;
98
+ }
99
+ const entry = {
100
+ description: asString(outputRecord.description),
101
+ value: asString(outputRecord.value),
102
+ };
103
+ if (entry.description !== undefined || entry.value !== undefined) {
104
+ result[name] = entry;
105
+ }
106
+ }
107
+ return Object.keys(result).length > 0 ? result : undefined;
108
+ }
109
+ function extractRuns(value) {
110
+ const runs = asRecord(value);
111
+ if (!runs) {
112
+ return undefined;
113
+ }
114
+ const stepsRaw = Array.isArray(runs.steps) ? runs.steps : [];
115
+ const steps = stepsRaw
116
+ .map((step) => extractStepFacts(step))
117
+ .filter((step) => step !== null);
118
+ const result = {
119
+ using: asString(runs.using),
120
+ main: asString(runs.main),
121
+ pre: asString(runs.pre),
122
+ post: asString(runs.post),
123
+ image: asString(runs.image),
124
+ args: asStringArray(runs.args),
125
+ steps: steps.length > 0 ? steps : undefined,
126
+ };
127
+ if (result.using === undefined &&
128
+ result.main === undefined &&
129
+ result.pre === undefined &&
130
+ result.post === undefined &&
131
+ result.image === undefined &&
132
+ result.args === undefined &&
133
+ result.steps === undefined) {
134
+ return undefined;
135
+ }
136
+ return result;
137
+ }
138
+ export function extractActionFacts(parsed) {
139
+ const root = asRecord(parsed);
140
+ if (!root) {
141
+ return null;
142
+ }
143
+ const runs = extractRuns(root.runs);
144
+ const inputs = extractInputs(root.inputs);
145
+ const outputs = extractOutputs(root.outputs);
146
+ const branding = asRecord(root.branding) ?? undefined;
147
+ if (runs === undefined &&
148
+ inputs === undefined &&
149
+ outputs === undefined &&
150
+ branding === undefined &&
151
+ asString(root.name) === undefined &&
152
+ asString(root.description) === undefined &&
153
+ asString(root.author) === undefined) {
154
+ return null;
155
+ }
156
+ return {
157
+ name: asString(root.name),
158
+ description: asString(root.description),
159
+ author: asString(root.author),
160
+ branding,
161
+ inputs,
162
+ outputs,
163
+ runs,
164
+ };
165
+ }
@@ -0,0 +1,39 @@
1
+ export interface ActionStepFacts {
2
+ id?: string;
3
+ name?: string;
4
+ uses?: string;
5
+ run?: string;
6
+ if?: string;
7
+ shell?: string;
8
+ workingDirectory?: string;
9
+ with?: Record<string, string>;
10
+ env?: Record<string, string>;
11
+ }
12
+ export interface ActionRunsFacts {
13
+ using?: string;
14
+ main?: string;
15
+ pre?: string;
16
+ post?: string;
17
+ image?: string;
18
+ args?: string[];
19
+ steps?: ActionStepFacts[];
20
+ }
21
+ export interface ActionInputFacts {
22
+ description?: string;
23
+ required?: boolean;
24
+ default?: string;
25
+ deprecationMessage?: string;
26
+ }
27
+ export interface ActionOutputFacts {
28
+ description?: string;
29
+ value?: string;
30
+ }
31
+ export interface ActionFacts {
32
+ name?: string;
33
+ description?: string;
34
+ author?: string;
35
+ branding?: Record<string, unknown>;
36
+ inputs?: Record<string, ActionInputFacts>;
37
+ outputs?: Record<string, ActionOutputFacts>;
38
+ runs?: ActionRunsFacts;
39
+ }
@@ -0,0 +1 @@
1
+ export {};