codegate-ai 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/README.md +48 -15
  2. package/dist/cli.js +41 -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.js +5 -1
  6. package/dist/config/inline-ignore.d.ts +11 -0
  7. package/dist/config/inline-ignore.js +57 -0
  8. package/dist/config/suppression-policy.d.ts +8 -0
  9. package/dist/config/suppression-policy.js +66 -1
  10. package/dist/config.d.ts +19 -1
  11. package/dist/config.js +160 -1
  12. package/dist/layer2-static/action/parser.d.ts +3 -0
  13. package/dist/layer2-static/action/parser.js +165 -0
  14. package/dist/layer2-static/action/types.d.ts +39 -0
  15. package/dist/layer2-static/action/types.js +1 -0
  16. package/dist/layer2-static/advisories/cache.d.ts +6 -0
  17. package/dist/layer2-static/advisories/cache.js +44 -0
  18. package/dist/layer2-static/advisories/cache.ts +62 -0
  19. package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
  20. package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
  21. package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
  22. package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
  23. package/dist/layer2-static/audits/registry.d.ts +3 -0
  24. package/dist/layer2-static/audits/registry.js +29 -0
  25. package/dist/layer2-static/audits/types.d.ts +12 -0
  26. package/dist/layer2-static/audits/types.js +1 -0
  27. package/dist/layer2-static/dependabot/parser.d.ts +3 -0
  28. package/dist/layer2-static/dependabot/parser.js +189 -0
  29. package/dist/layer2-static/dependabot/types.d.ts +50 -0
  30. package/dist/layer2-static/dependabot/types.js +1 -0
  31. package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
  32. package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
  33. package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
  34. package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
  35. package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
  36. package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
  37. package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
  38. package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
  39. package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
  40. package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
  41. package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
  42. package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
  43. package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
  44. package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
  45. package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
  46. package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
  47. package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
  48. package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
  49. package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
  50. package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
  51. package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
  52. package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
  53. package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
  54. package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
  55. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
  56. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
  57. package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
  58. package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
  59. package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
  60. package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
  61. package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
  62. package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
  63. package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
  64. package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
  65. package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
  66. package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
  67. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
  68. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
  69. package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
  70. package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
  71. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
  72. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
  73. package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
  74. package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
  75. package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
  76. package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
  77. package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
  78. package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
  79. package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
  80. package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
  81. package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
  82. package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
  83. package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
  84. package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
  85. package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
  86. package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
  87. package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
  88. package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
  89. package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
  90. package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
  91. package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
  92. package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
  93. package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
  94. package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
  95. package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
  96. package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
  97. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
  98. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
  99. package/dist/layer2-static/engine.d.ts +9 -1
  100. package/dist/layer2-static/engine.js +490 -50
  101. package/dist/layer2-static/github/cache.d.ts +8 -0
  102. package/dist/layer2-static/github/cache.js +41 -0
  103. package/dist/layer2-static/github/client.d.ts +17 -0
  104. package/dist/layer2-static/github/client.js +43 -0
  105. package/dist/layer2-static/rules/claude-code.json +16 -0
  106. package/dist/layer2-static/rules/codex.json +16 -0
  107. package/dist/layer2-static/rules/common.json +15 -0
  108. package/dist/layer2-static/rules/copilot.json +15 -0
  109. package/dist/layer2-static/rules/cursor.json +15 -0
  110. package/dist/layer2-static/rules/opencode.json +15 -0
  111. package/dist/layer2-static/workflow/injection-sinks.json +5 -0
  112. package/dist/layer2-static/workflow/parser.d.ts +3 -0
  113. package/dist/layer2-static/workflow/parser.js +88 -0
  114. package/dist/layer2-static/workflow/types.d.ts +15 -0
  115. package/dist/layer2-static/workflow/types.js +1 -0
  116. package/dist/pipeline.d.ts +1 -1
  117. package/dist/pipeline.js +3 -3
  118. package/dist/reporter/sarif.js +23 -5
  119. package/dist/reporter/terminal.js +12 -0
  120. package/dist/scan.d.ts +7 -1
  121. package/dist/scan.js +154 -40
  122. package/dist/types/finding.d.ts +1 -1
  123. package/dist/types/finding.js +5 -0
  124. package/package.json +1 -1
package/README.md CHANGED
@@ -120,21 +120,26 @@ See the [Configuration](#configuration) section for full settings and examples.
120
120
 
121
121
  ## `scan` Command Flags
122
122
 
123
- | Flag | Purpose |
124
- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
125
- | `--deep` | Enable Layer 3 dynamic analysis. |
126
- | `--remediate` | Enter remediation mode after scan. |
127
- | `--fix-safe` | Auto-fix unambiguous critical findings. |
128
- | `--dry-run` | Show proposed fixes but write nothing. |
129
- | `--patch` | Generate a patch file for review workflows. |
130
- | `--no-tui` | Disable TUI and interactive prompts. |
131
- | `--format <type>` | Output format: `terminal`, `json`, `sarif`, `markdown`, `html`. |
132
- | `--output <path>` | Write report to file instead of stdout. |
133
- | `--verbose` | Show extended output in terminal format. |
134
- | `--config <path>` | Use a specific global config file path. |
135
- | `--force` | Skip interactive confirmations. |
136
- | `--include-user-scope` | Force-enable user/home AI tool config paths for this run (useful if config disables user-scope scanning). |
137
- | `--reset-state` | Clear persisted scan-state history and exit. |
123
+ | Flag | Purpose |
124
+ | ----------------------- | --------------------------------------------------------------------------------------------------------- |
125
+ | `--deep` | Enable Layer 3 dynamic analysis. |
126
+ | `--remediate` | Enter remediation mode after scan. |
127
+ | `--fix-safe` | Auto-fix unambiguous critical findings. |
128
+ | `--dry-run` | Show proposed fixes but write nothing. |
129
+ | `--patch` | Generate a patch file for review workflows. |
130
+ | `--no-tui` | Disable TUI and interactive prompts. |
131
+ | `--format <type>` | Output format: `terminal`, `json`, `sarif`, `markdown`, `html`. |
132
+ | `--output <path>` | Write report to file instead of stdout. |
133
+ | `--verbose` | Show extended output in terminal format. |
134
+ | `--config <path>` | Use a specific global config file path. |
135
+ | `--force` | Skip interactive confirmations. |
136
+ | `--include-user-scope` | Force-enable user/home AI tool config paths for this run (useful if config disables user-scope scanning). |
137
+ | `--collect <mode>` | Collection scope mode (`default`, `project`, `user`, `explicit`, `all`). Repeatable. |
138
+ | `--strict-collection` | Treat parse failures in collected inputs as high-severity findings. |
139
+ | `--persona <type>` | Audit sensitivity (`regular`, `pedantic`, `auditor`). |
140
+ | `--runtime-mode <mode>` | Runtime mode for optional online audits (`offline`, `online`, `online-no-audits`). |
141
+ | `--workflow-audits` | Enable CI/CD audit pack for GitHub workflow, action, and Dependabot inputs. |
142
+ | `--reset-state` | Clear persisted scan-state history and exit. |
138
143
 
139
144
  Examples:
140
145
 
@@ -148,9 +153,37 @@ codegate scan . --deep --force
148
153
  codegate scan . --remediate
149
154
  codegate scan . --fix-safe
150
155
  codegate scan . --remediate --dry-run --patch
156
+ codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online
157
+ codegate scan . --workflow-audits --strict-collection
151
158
  codegate scan . --reset-state
152
159
  ```
153
160
 
161
+ ## Workflow Audit Pack
162
+
163
+ CodeGate can audit GitHub Actions workflows when `--workflow-audits` is enabled.
164
+
165
+ Current checks include:
166
+
167
+ - Unpinned external action references (`uses: owner/repo@tag` instead of commit SHA)
168
+ - High-risk triggers (`pull_request_target`, `workflow_run`)
169
+ - Overly broad permissions (`write-all` and explicit write grants)
170
+ - Template expression injection patterns in run steps and known sink inputs
171
+ - Known vulnerable action references (online runtime mode)
172
+ - Dependabot cooldown and execution-risk checks
173
+ - Workflow hygiene checks (concurrency gates, obfuscation, unsafe conditional trust)
174
+
175
+ Track the current workflow-audit coverage and backlog in the [workflow audit parity checklist](docs/workflow-audit-parity-checklist.md).
176
+ Real public validation fixtures and source provenance are documented in [workflow audit real-case corpus](docs/workflow-audit-real-cases.md).
177
+
178
+ Examples:
179
+
180
+ ```bash
181
+ codegate scan . --workflow-audits
182
+ codegate scan . --workflow-audits --collect project --persona auditor
183
+ codegate scan . --workflow-audits --runtime-mode online
184
+ codegate scan . --workflow-audits --collect-kind dependabot
185
+ ```
186
+
154
187
  ## `scan-content` Command
155
188
 
156
189
  `codegate scan-content <content...>` scans inline content directly from the command line. It is useful when you want to inspect JSON, YAML, TOML, Markdown, or plain text before writing it to disk or installing it into a tool configuration.
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { dirname, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command, Option } from "commander";
9
- import { DEFAULT_CONFIG, OUTPUT_FORMATS, resolveEffectiveConfig, } from "./config.js";
9
+ import { DEFAULT_CONFIG, OUTPUT_FORMATS, PERSONAS, RUNTIME_MODES, SCAN_COLLECTION_MODES, SCAN_COLLECTION_KINDS, resolveEffectiveConfig, } from "./config.js";
10
10
  import { APP_NAME } from "./index.js";
11
11
  import { runSandboxCommand } from "./layer3-dynamic/sandbox.js";
12
12
  import { loadKnowledgeBase } from "./layer1-discovery/knowledge-base.js";
@@ -127,6 +127,8 @@ const defaultCliDeps = {
127
127
  }),
128
128
  prepareScanDiscovery: (scanTarget, config, options) => createScanDiscoveryContext(scanTarget, undefined, {
129
129
  includeUserScope: config?.scan_user_scope === true,
130
+ collectModes: config?.scan_collection_modes,
131
+ collectKinds: config?.scan_collection_kinds,
130
132
  parseSelected: true,
131
133
  explicitCandidates: options?.explicitCandidates,
132
134
  }),
@@ -158,6 +160,8 @@ const defaultCliDeps = {
158
160
  ? discoverDeepScanResourcesFromContext(discoveryContext)
159
161
  : discoverDeepScanResources(scanTarget, undefined, {
160
162
  includeUserScope: config?.scan_user_scope === true,
163
+ collectModes: config?.scan_collection_modes,
164
+ collectKinds: config?.scan_collection_kinds,
161
165
  }),
162
166
  discoverLocalTextTargets: (_scanTarget, _config, discoveryContext) => discoveryContext ? discoverLocalTextAnalysisTargetsFromContext(discoveryContext) : [],
163
167
  // Deep resource execution never makes outbound network calls.
@@ -198,6 +202,20 @@ function addScanCommand(program, version, deps) {
198
202
  .option("--config <path>", "use a specific global config file")
199
203
  .option("--force", "skip interactive confirmations")
200
204
  .option("--include-user-scope", "include user/home AI tool config paths in scan")
205
+ .addOption(new Option("--collect <mode>", "collection mode (repeatable): default, project, user, explicit, all")
206
+ .choices([...SCAN_COLLECTION_MODES])
207
+ .argParser((value, previous = []) => [...previous, value]))
208
+ .addOption(new Option("--collect-kind <kind>", "collection kind (repeatable): workflows, actions, dependabot")
209
+ .choices([...SCAN_COLLECTION_KINDS])
210
+ .argParser((value, previous = []) => [...previous, value]))
211
+ .option("--strict-collection", "treat parse failures in collected inputs as high severity")
212
+ .addOption(new Option("--persona <type>", "audit sensitivity persona")
213
+ .choices([...PERSONAS])
214
+ .argParser((value) => value))
215
+ .addOption(new Option("--runtime-mode <mode>", "runtime network mode for optional online audits")
216
+ .choices([...RUNTIME_MODES])
217
+ .argParser((value) => value))
218
+ .option("--workflow-audits", "enable workflow security audit pack for .github/workflows")
201
219
  .option("--skill <name>", "select one skill directory when scanning a skills index repo URL")
202
220
  .option("--reset-state", "clear persisted scan-state history and exit")
203
221
  .addHelpText("after", renderExampleHelp([
@@ -205,6 +223,7 @@ function addScanCommand(program, version, deps) {
205
223
  "codegate scan ./skills/security-review/SKILL.md",
206
224
  "codegate scan https://github.com/owner/repo",
207
225
  "codegate scan https://github.com/owner/repo --skill security-review",
226
+ "codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online",
208
227
  "codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
209
228
  "codegate scan https://example.com/security-review/SKILL.md --format json",
210
229
  ]))
@@ -220,6 +239,7 @@ function addScanCommand(program, version, deps) {
220
239
  };
221
240
  let resolvedTarget;
222
241
  try {
242
+ const scanOptions = options;
223
243
  const resolveTarget = deps.resolveScanTarget ??
224
244
  ((input) => resolveScanTarget(input));
225
245
  resolvedTarget = await resolveTarget({
@@ -237,12 +257,26 @@ function addScanCommand(program, version, deps) {
237
257
  scanTarget,
238
258
  cli: cliConfig,
239
259
  });
240
- const config = options.includeUserScope === true
241
- ? {
242
- ...baseConfig,
243
- scan_user_scope: true,
244
- }
245
- : baseConfig;
260
+ const config = {
261
+ ...baseConfig,
262
+ scan_collection_modes: options.collect && options.collect.length > 0
263
+ ? options.collect
264
+ : baseConfig.scan_collection_modes,
265
+ scan_collection_kinds: (scanOptions.collectKind && scanOptions.collectKind.length > 0
266
+ ? scanOptions.collectKind
267
+ : baseConfig.scan_collection_kinds),
268
+ strict_collection: options.strictCollection === true
269
+ ? true
270
+ : (baseConfig.strict_collection ?? DEFAULT_CONFIG.strict_collection),
271
+ persona: options.persona ?? baseConfig.persona,
272
+ runtime_mode: options.runtimeMode ?? baseConfig.runtime_mode,
273
+ workflow_audits: {
274
+ enabled: options.workflowAudits === true
275
+ ? true
276
+ : (baseConfig.workflow_audits?.enabled ?? false),
277
+ },
278
+ scan_user_scope: options.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
279
+ };
246
280
  if (options.resetState) {
247
281
  const reset = deps.resetScanState ?? ((path) => resetScanState(path));
248
282
  await reset(config.scan_state_path);
@@ -1,4 +1,4 @@
1
- import { type CodeGateConfig, type OutputFormat } from "../config.js";
1
+ import { type AuditPersona, type CodeGateConfig, type OutputFormat, type RuntimeMode, type ScanCollectionMode } from "../config.js";
2
2
  import { type MetaAgentCommand, type MetaAgentTool } from "../layer3-dynamic/command-builder.js";
3
3
  import type { LocalTextAnalysisTarget } from "../layer3-dynamic/local-text-analysis.js";
4
4
  import type { ResourceFetchResult } from "../layer3-dynamic/resource-fetcher.js";
@@ -21,6 +21,11 @@ export interface ScanCommandOptions {
21
21
  resetState?: boolean;
22
22
  includeUserScope?: boolean;
23
23
  skill?: string;
24
+ collect?: ScanCollectionMode[];
25
+ strictCollection?: boolean;
26
+ persona?: AuditPersona;
27
+ runtimeMode?: RuntimeMode;
28
+ workflowAudits?: boolean;
24
29
  }
25
30
  export interface ScanRunnerInput {
26
31
  version: string;
@@ -1,5 +1,5 @@
1
1
  import { resolve } from "node:path";
2
- import { applyConfigPolicy } from "../config.js";
2
+ import { applyConfigPolicy, } from "../config.js";
3
3
  import { buildMetaAgentCommand, } from "../layer3-dynamic/command-builder.js";
4
4
  import { supportsAgentLocalTextAnalysis } from "../layer3-dynamic/local-text-analysis.js";
5
5
  import { buildLocalTextAnalysisPrompt, buildSecurityAnalysisPrompt, } from "../layer3-dynamic/meta-agent.js";
@@ -20,7 +20,7 @@ export async function executeScanContentCommand(input, deps) {
20
20
  throw new Error(parsed.error);
21
21
  }
22
22
  const kbVersion = loadKnowledgeBase().schemaVersion;
23
- const report = applyConfigPolicy(runStaticPipeline({
23
+ const report = applyConfigPolicy(await runStaticPipeline({
24
24
  version: input.version,
25
25
  kbVersion,
26
26
  scanTarget: `scan-content:${input.type}`,
@@ -48,6 +48,10 @@ export async function executeScanContentCommand(input, deps) {
48
48
  rulePackPaths: input.config.rule_pack_paths,
49
49
  allowedRules: input.config.allowed_rules,
50
50
  skipRules: input.config.skip_rules,
51
+ persona: input.config.persona,
52
+ runtimeMode: input.config.runtime_mode,
53
+ workflowAuditsEnabled: input.config.workflow_audits?.enabled === true,
54
+ rulePolicies: input.config.rules,
51
55
  },
52
56
  }), input.config);
53
57
  deps.stdout(renderByFormat(input.config.output_format, report));
@@ -0,0 +1,11 @@
1
+ import type { Finding } from "../types/finding.js";
2
+ export interface InlineIgnoreDirectiveSet {
3
+ rules: Set<string>;
4
+ ruleLines: Map<string, Set<number>>;
5
+ }
6
+ export type InlineIgnoreMap = Map<string, InlineIgnoreDirectiveSet>;
7
+ export declare function collectInlineIgnoreDirectives(files: Array<{
8
+ filePath: string;
9
+ textContent: string;
10
+ }>): InlineIgnoreMap;
11
+ export declare function applyInlineIgnoreDirectives<T extends Finding>(findings: T[], directives: InlineIgnoreMap): T[];
@@ -0,0 +1,57 @@
1
+ function normalizeRuleId(value) {
2
+ const trimmed = value.trim();
3
+ return trimmed.length > 0 ? trimmed : null;
4
+ }
5
+ function addDirective(target, ruleId, line) {
6
+ target.rules.add(ruleId);
7
+ const lines = target.ruleLines.get(ruleId) ?? new Set();
8
+ lines.add(line);
9
+ target.ruleLines.set(ruleId, lines);
10
+ }
11
+ function parseDirectiveRules(raw) {
12
+ return raw
13
+ .split(",")
14
+ .map((entry) => normalizeRuleId(entry))
15
+ .filter((entry) => entry !== null);
16
+ }
17
+ export function collectInlineIgnoreDirectives(files) {
18
+ const directives = new Map();
19
+ const pattern = /codegate:\s*ignore\[([^\]]+)\]/giu;
20
+ for (const file of files) {
21
+ const lines = file.textContent.split(/\r?\n/u);
22
+ let directiveSet = directives.get(file.filePath);
23
+ for (let index = 0; index < lines.length; index += 1) {
24
+ const line = lines[index] ?? "";
25
+ pattern.lastIndex = 0;
26
+ for (const match of line.matchAll(pattern)) {
27
+ const ruleIds = parseDirectiveRules(match[1] ?? "");
28
+ if (ruleIds.length === 0) {
29
+ continue;
30
+ }
31
+ if (!directiveSet) {
32
+ directiveSet = {
33
+ rules: new Set(),
34
+ ruleLines: new Map(),
35
+ };
36
+ directives.set(file.filePath, directiveSet);
37
+ }
38
+ for (const ruleId of ruleIds) {
39
+ addDirective(directiveSet, ruleId, index + 1);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ return directives;
45
+ }
46
+ export function applyInlineIgnoreDirectives(findings, directives) {
47
+ return findings.map((finding) => {
48
+ const set = directives.get(finding.file_path);
49
+ if (!set || !set.rules.has(finding.rule_id)) {
50
+ return finding;
51
+ }
52
+ return {
53
+ ...finding,
54
+ suppressed: true,
55
+ };
56
+ });
57
+ }
@@ -2,13 +2,21 @@ import type { Finding } from "../types/finding.js";
2
2
  export interface SuppressionRule {
3
3
  rule_id?: string;
4
4
  file_path?: string;
5
+ location?: string;
5
6
  severity?: Finding["severity"];
6
7
  category?: Finding["category"];
7
8
  cwe?: string;
8
9
  fingerprint?: string;
9
10
  }
11
+ export interface RulePolicyConfig {
12
+ disable?: boolean;
13
+ ignore?: readonly string[];
14
+ config?: Record<string, unknown>;
15
+ }
16
+ export type RulePolicyMap = Record<string, RulePolicyConfig>;
10
17
  export interface SuppressionPolicy {
11
18
  suppress_findings?: readonly string[];
12
19
  suppression_rules?: readonly SuppressionRule[];
20
+ rule_policies?: RulePolicyMap;
13
21
  }
14
22
  export declare function applySuppressionPolicy<T extends Finding>(findings: T[], policy: SuppressionPolicy): T[];
@@ -38,6 +38,33 @@ function globToRegExp(glob) {
38
38
  function matchesGlob(value, glob) {
39
39
  return globToRegExp(glob).test(value.replaceAll("\\", "/"));
40
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
+ }
41
68
  function matchesSuppressionRule(finding, rule) {
42
69
  const ruleId = normalizeString(rule.rule_id);
43
70
  if (ruleId && finding.rule_id !== ruleId) {
@@ -47,6 +74,21 @@ function matchesSuppressionRule(finding, rule) {
47
74
  if (filePath && !matchesGlob(finding.file_path, filePath)) {
48
75
  return false;
49
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
+ }
50
92
  const severity = rule.severity;
51
93
  if (severity && finding.severity !== severity) {
52
94
  return false;
@@ -65,17 +107,40 @@ function matchesSuppressionRule(finding, rule) {
65
107
  }
66
108
  return true;
67
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
+ }
68
129
  export function applySuppressionPolicy(findings, policy) {
69
130
  const legacySuppressions = new Set((policy.suppress_findings ?? [])
70
131
  .map((findingId) => normalizeString(findingId))
71
132
  .filter((findingId) => findingId !== undefined));
72
133
  const rules = policy.suppression_rules ?? [];
134
+ const rulePolicies = policy.rule_policies ?? {};
73
135
  return findings.map((finding) => {
74
136
  const ruleMatch = rules.some((rule) => matchesSuppressionRule(finding, rule));
75
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;
76
141
  return {
77
142
  ...finding,
78
- suppressed: finding.suppressed || legacyMatch || ruleMatch,
143
+ suppressed: finding.suppressed || legacyMatch || ruleMatch || ruleDisabled || ruleIgnoreMatch,
79
144
  };
80
145
  });
81
146
  }
package/dist/config.d.ts CHANGED
@@ -1,8 +1,16 @@
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
+ import { type RulePolicyMap, type SuppressionRule } from "./config/suppression-policy.js";
4
4
  export declare const OUTPUT_FORMATS: readonly ["terminal", "json", "sarif", "markdown", "html"];
5
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];
6
14
  export declare const SEVERITY_THRESHOLDS: readonly ["critical", "high", "medium", "low", "info"];
7
15
  export type SeverityThreshold = (typeof SEVERITY_THRESHOLDS)[number];
8
16
  export interface TuiConfig {
@@ -15,6 +23,9 @@ export interface ToolDiscoveryConfig {
15
23
  agent_paths: Record<string, string>;
16
24
  skip_tools: string[];
17
25
  }
26
+ export interface WorkflowAuditConfig {
27
+ enabled: boolean;
28
+ }
18
29
  export interface CodeGateConfig {
19
30
  severity_threshold: SeverityThreshold;
20
31
  auto_proceed_below_threshold: boolean;
@@ -36,6 +47,13 @@ export interface CodeGateConfig {
36
47
  rule_pack_paths?: string[];
37
48
  allowed_rules?: string[];
38
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;
39
57
  suppress_findings: string[];
40
58
  suppression_rules?: SuppressionRule[];
41
59
  }
package/dist/config.js CHANGED
@@ -3,8 +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
+ import { applySuppressionPolicy, } from "./config/suppression-policy.js";
7
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"];
8
12
  export const SEVERITY_THRESHOLDS = ["critical", "high", "medium", "low", "info"];
9
13
  export const DEFAULT_CONFIG = {
10
14
  severity_threshold: "high",
@@ -38,6 +42,11 @@ export const DEFAULT_CONFIG = {
38
42
  rule_pack_paths: [],
39
43
  allowed_rules: [],
40
44
  skip_rules: [],
45
+ strict_collection: false,
46
+ scan_collection_modes: ["default"],
47
+ persona: "regular",
48
+ runtime_mode: "offline",
49
+ workflow_audits: { enabled: false },
41
50
  suppress_findings: [],
42
51
  suppression_rules: [],
43
52
  };
@@ -60,6 +69,146 @@ function normalizeOptionalPath(value) {
60
69
  const trimmed = value.trim();
61
70
  return trimmed.length > 0 ? trimmed : undefined;
62
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
+ }
63
212
  function unique(values) {
64
213
  const merged = values.flatMap((entry) => entry ?? []);
65
214
  const seen = new Set();
@@ -183,6 +332,15 @@ export function resolveEffectiveConfig(options) {
183
332
  globalConfig.skip_rules,
184
333
  projectConfig.skip_rules,
185
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
+ },
186
344
  suppress_findings: unique([
187
345
  DEFAULT_CONFIG.suppress_findings,
188
346
  globalConfig.suppress_findings,
@@ -202,6 +360,7 @@ export function applyConfigPolicy(report, config) {
202
360
  const findings = applySuppressionPolicy(report.findings, {
203
361
  suppress_findings: config.suppress_findings,
204
362
  suppression_rules: config.suppression_rules,
363
+ rule_policies: config.rules,
205
364
  }).map((finding) => ({
206
365
  ...finding,
207
366
  owasp: config.owasp_mapping ? finding.owasp : [],
@@ -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;