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
@@ -15,6 +15,27 @@ function appendLabeledText(lines, label, value) {
15
15
  }
16
16
  lines.push(` ${label}: ${value}`);
17
17
  }
18
+ function appendMetadata(lines, metadata) {
19
+ if (!metadata) {
20
+ return;
21
+ }
22
+ const hasContent = (metadata.sources?.length ?? 0) > 0 ||
23
+ (metadata.sinks?.length ?? 0) > 0 ||
24
+ (metadata.referenced_secrets?.length ?? 0) > 0 ||
25
+ (metadata.risk_tags?.length ?? 0) > 0 ||
26
+ typeof metadata.origin === "string";
27
+ if (!hasContent) {
28
+ return;
29
+ }
30
+ lines.push(" Metadata:");
31
+ appendLabeledList(lines, "Sources", metadata.sources ?? []);
32
+ appendLabeledList(lines, "Sinks", metadata.sinks ?? []);
33
+ appendLabeledList(lines, "Referenced secrets", metadata.referenced_secrets ?? []);
34
+ appendLabeledList(lines, "Risk tags", metadata.risk_tags ?? []);
35
+ if (metadata.origin) {
36
+ appendLabeledText(lines, "Origin", metadata.origin);
37
+ }
38
+ }
18
39
  function appendEvidence(lines, evidence) {
19
40
  const evidenceLines = evidence.split("\n");
20
41
  if (evidenceLines.length === 1) {
@@ -55,11 +76,26 @@ function appendFinding(lines, report, options, finding) {
55
76
  if (verbose) {
56
77
  lines.push(` Rule: ${finding.rule_id}`);
57
78
  lines.push(` Finding ID: ${finding.finding_id}`);
79
+ if (finding.fingerprint) {
80
+ lines.push(` Fingerprint: ${finding.fingerprint}`);
81
+ }
58
82
  lines.push(` Category: ${finding.category} | Layer: ${finding.layer} | Confidence: ${finding.confidence}`);
59
83
  const formattedLocation = formatLocation(finding.location);
60
84
  if (formattedLocation) {
61
85
  lines.push(` Location: ${formattedLocation}`);
62
86
  }
87
+ if ((finding.affected_locations?.length ?? 0) > 0) {
88
+ lines.push(" Affected locations:");
89
+ for (const location of finding.affected_locations ?? []) {
90
+ const path = toAbsoluteDisplayPath(report.scan_target, location.file_path);
91
+ const locationText = formatLocation({
92
+ field: location.location?.field,
93
+ line: location.location?.line,
94
+ column: location.location?.column,
95
+ });
96
+ lines.push(` - ${path}${locationText ? ` (${locationText})` : ""}`);
97
+ }
98
+ }
63
99
  if (finding.cve) {
64
100
  lines.push(` CVE: ${finding.cve}`);
65
101
  }
@@ -70,6 +106,7 @@ function appendFinding(lines, report, options, finding) {
70
106
  if (finding.remediation_actions.length > 0) {
71
107
  lines.push(` Remediation: ${finding.remediation_actions.join(", ")}`);
72
108
  }
109
+ appendMetadata(lines, finding.metadata);
73
110
  }
74
111
  if (finding.layer === "L3" && finding.source_config) {
75
112
  const fieldSuffix = finding.source_config.field ? ` (${finding.source_config.field})` : "";
@@ -0,0 +1,8 @@
1
+ export interface SparseFetchPlan {
2
+ sparsePaths: string[];
3
+ }
4
+ export interface BuildSparseFetchPlanInput {
5
+ preferredSkill?: string;
6
+ inferredSkill?: string;
7
+ }
8
+ export declare function buildSparseFetchPlan(source: string, input?: BuildSparseFetchPlanInput): SparseFetchPlan | null;
@@ -0,0 +1,30 @@
1
+ import { isLikelyGitRepoUrl } from "./helpers.js";
2
+ function normalizeSkillName(value) {
3
+ const trimmed = value?.trim();
4
+ if (!trimmed) {
5
+ return null;
6
+ }
7
+ return trimmed.replace(/^skills\//iu, "").replace(/^\/+/u, "");
8
+ }
9
+ function selectSkill(input) {
10
+ return normalizeSkillName(input.preferredSkill) ?? normalizeSkillName(input.inferredSkill);
11
+ }
12
+ export function buildSparseFetchPlan(source, input = {}) {
13
+ let url;
14
+ try {
15
+ url = new URL(source);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ if (!isLikelyGitRepoUrl(url)) {
21
+ return null;
22
+ }
23
+ const selectedSkill = selectSkill(input);
24
+ if (!selectedSkill) {
25
+ return null;
26
+ }
27
+ return {
28
+ sparsePaths: ["/*", "!/*/", "/.*/**", "/hooks/**", `/skills/${selectedSkill}/**`],
29
+ };
30
+ }
@@ -2,16 +2,71 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, statSync
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { tmpdir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
+ import { buildSparseFetchPlan } from "./fetch-plan.js";
5
6
  import { cleanupTempDir, collectExplicitCandidates, copyDirectoryRecursive, extractSkillFromRepoPath, inferRemoteCandidateFormat, inferLocalFileStagePath, inferRemoteFileStagePath, inferToolFromReportPath, parseGitHubFileSource, preserveTailSegments, shouldStageContainingFolder, } from "./helpers.js";
6
- function cloneRepository(source, destination) {
7
- const result = spawnSync("git", ["clone", "--depth", "1", "--filter=blob:none", source, destination], {
7
+ function runGit(args) {
8
+ const result = spawnSync("git", args, {
9
+ encoding: "utf8",
10
+ });
11
+ if (result.status !== 0) {
12
+ const stderr = result.stderr?.trim();
13
+ throw new Error(stderr && stderr.length > 0 ? stderr : `git ${args.join(" ")} failed`);
14
+ }
15
+ }
16
+ function cloneRepository(source, destination, sparsePaths) {
17
+ const cloneArgs = ["clone", "--depth", "1", "--filter=blob:none"];
18
+ if (sparsePaths && sparsePaths.length > 0) {
19
+ cloneArgs.push("--no-checkout");
20
+ }
21
+ cloneArgs.push(source, destination);
22
+ const result = spawnSync("git", cloneArgs, {
8
23
  encoding: "utf8",
9
24
  });
10
25
  if (result.status !== 0) {
11
26
  const stderr = result.stderr?.trim();
12
27
  throw new Error(stderr && stderr.length > 0 ? stderr : `git clone failed for ${source}`);
13
28
  }
14
- cleanupTempDir(join(destination, ".git"));
29
+ if (!sparsePaths || sparsePaths.length === 0) {
30
+ cleanupTempDir(join(destination, ".git"));
31
+ return;
32
+ }
33
+ try {
34
+ runGit(["-C", destination, "sparse-checkout", "init", "--no-cone"]);
35
+ runGit(["-C", destination, "sparse-checkout", "set", "--no-cone", ...sparsePaths]);
36
+ runGit(["-C", destination, "checkout", "--force", "HEAD"]);
37
+ cleanupTempDir(join(destination, ".git"));
38
+ return;
39
+ }
40
+ catch (error) {
41
+ cleanupTempDir(destination);
42
+ const fallback = spawnSync("git", ["clone", "--depth", "1", "--filter=blob:none", source, destination], {
43
+ encoding: "utf8",
44
+ });
45
+ if (fallback.status !== 0) {
46
+ const stderr = fallback.stderr?.trim();
47
+ throw new Error(stderr && stderr.length > 0 ? stderr : `git clone failed for ${source}`, {
48
+ cause: error,
49
+ });
50
+ }
51
+ cleanupTempDir(join(destination, ".git"));
52
+ }
53
+ }
54
+ function cloneSparseRepository(source, destination, sparsePaths) {
55
+ cloneRepository(source, destination, sparsePaths);
56
+ }
57
+ function cloneFullRepository(source, destination) {
58
+ cloneRepository(source, destination);
59
+ }
60
+ function stageSparseClone(source, destination, preferredSkill, inferredSkill) {
61
+ const sparsePlan = buildSparseFetchPlan(source, {
62
+ preferredSkill,
63
+ inferredSkill,
64
+ });
65
+ if (!sparsePlan) {
66
+ cloneFullRepository(source, destination);
67
+ return;
68
+ }
69
+ cloneSparseRepository(source, destination, sparsePlan.sparsePaths);
15
70
  }
16
71
  export function stageLocalFile(absolutePath) {
17
72
  const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-target-"));
@@ -132,7 +187,7 @@ export async function cloneGitRepo(rawTarget, options = {}) {
132
187
  const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-"));
133
188
  const repoDir = join(tempRoot, "repo");
134
189
  try {
135
- cloneRepository(rawTarget, repoDir);
190
+ stageSparseClone(rawTarget, repoDir, options.preferredSkill, options.inferredSkill);
136
191
  return await stageSkillAwareRepository(tempRoot, repoDir, options.displayTarget ?? rawTarget, options);
137
192
  }
138
193
  catch (error) {
@@ -144,7 +199,7 @@ export function stageRepoSubdirectory(repoUrl, filePath, displayTarget) {
144
199
  const tempRoot = mkdtempSync(join(tmpdir(), "codegate-scan-repo-file-"));
145
200
  const repoDir = join(tempRoot, "repo");
146
201
  try {
147
- cloneRepository(repoUrl, repoDir);
202
+ stageSparseClone(repoUrl, repoDir, undefined, extractSkillFromRepoPath(filePath) ?? undefined);
148
203
  const inferredSkill = extractSkillFromRepoPath(filePath);
149
204
  if (inferredSkill) {
150
205
  const stageRoot = join(tempRoot, "staged");
package/dist/scan.d.ts CHANGED
@@ -4,7 +4,7 @@ import { type KnowledgeBaseLoadResult } from "./layer1-discovery/knowledge-base.
4
4
  import { type WalkResult } from "./layer1-discovery/file-walker.js";
5
5
  import type { DiscoveryFormat } from "./types/discovery.js";
6
6
  import type { CodeGateReport } from "./types/report.js";
7
- import type { CodeGateConfig } from "./config.js";
7
+ import type { CodeGateConfig, ScanCollectionKind, ScanCollectionMode } from "./config.js";
8
8
  import type { DeepScanResource } from "./pipeline.js";
9
9
  export interface ScanEngineInput {
10
10
  version: string;
@@ -18,10 +18,14 @@ export interface ScanEngineInput {
18
18
  export interface DeepScanDiscoveryOptions {
19
19
  includeUserScope?: boolean;
20
20
  homeDir?: string;
21
+ collectModes?: ScanCollectionMode[];
22
+ collectKinds?: ScanCollectionKind[];
21
23
  }
22
24
  export interface ScanSurfaceOptions {
23
25
  includeUserScope?: boolean;
24
26
  homeDir?: string;
27
+ collectModes?: ScanCollectionMode[];
28
+ collectKinds?: ScanCollectionKind[];
25
29
  }
26
30
  export interface ScanDiscoveryCandidate {
27
31
  reportPath: string;
@@ -45,6 +49,8 @@ export interface ScanDiscoveryContextOptions {
45
49
  homeDir?: string;
46
50
  parseSelected?: boolean;
47
51
  explicitCandidates?: ScanDiscoveryCandidate[];
52
+ collectModes?: ScanCollectionMode[];
53
+ collectKinds?: ScanCollectionKind[];
48
54
  }
49
55
  export declare function discoverDeepScanResources(scanTarget: string, kbInput?: KnowledgeBaseLoadResult, options?: DeepScanDiscoveryOptions): DeepScanResource[];
50
56
  export declare function createScanDiscoveryContext(scanTarget: string, kbInput?: KnowledgeBaseLoadResult, options?: ScanDiscoveryContextOptions): ScanDiscoveryContext;
package/dist/scan.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { join, relative, resolve, sep } from "node:path";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
4
  import { collectLocalTextAnalysisTargets, } from "./layer3-dynamic/local-text-analysis.js";
5
5
  import { runStaticPipeline } from "./pipeline.js";
6
6
  import { applyReportSummary } from "./report-summary.js";
@@ -9,11 +9,28 @@ import { loadKnowledgeBase, } from "./layer1-discovery/knowledge-base.js";
9
9
  import { detectTools } from "./layer1-discovery/tool-detector.js";
10
10
  import { walkProjectTree } from "./layer1-discovery/file-walker.js";
11
11
  import { evaluateScanStateSnapshots, extractMcpServerSnapshots, loadScanState, saveScanState, } from "./layer2-static/state/scan-state.js";
12
+ import { applyInlineIgnoreDirectives, collectInlineIgnoreDirectives, } from "./config/inline-ignore.js";
13
+ import { isGitHubDependabotPath } from "./layer2-static/dependabot/parser.js";
12
14
  const MCP_SERVER_CONTAINER_KEYS = ["mcpServers", "mcp_servers", "context_servers"];
13
15
  const REMOTE_MCP_SERVER_ARRAY_KEYS = ["remoteMCPServers", "remote_mcp_servers"];
14
16
  const USER_SCOPE_WILDCARD_MAX_DEPTH = 6;
15
17
  const USER_SCOPE_WILDCARD_MAX_FILES = 500;
16
18
  const INFERRED_ARTIFACT_RULES = [
19
+ {
20
+ pattern: /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu,
21
+ format: "yaml",
22
+ tool: "github-actions",
23
+ },
24
+ {
25
+ pattern: /(?:^|\/)\.github\/dependabot\.ya?ml$/iu,
26
+ format: "yaml",
27
+ tool: "dependabot",
28
+ },
29
+ {
30
+ pattern: /(?:^|\/)action\.ya?ml$/iu,
31
+ format: "yaml",
32
+ tool: "github-actions",
33
+ },
17
34
  { pattern: /(?:^|\/)agents\.md$/iu, format: "markdown", tool: "claude-code" },
18
35
  { pattern: /(?:^|\/)claude\.md$/iu, format: "markdown", tool: "claude-code" },
19
36
  { pattern: /(?:^|\/)codex\.md$/iu, format: "markdown", tool: "codex-cli" },
@@ -37,6 +54,12 @@ function wildcardToRegex(pattern) {
37
54
  function normalizePathForMatch(path) {
38
55
  return path.split(sep).join("/");
39
56
  }
57
+ function normalizeCollectionKinds(input) {
58
+ if (!input || input.length === 0) {
59
+ return undefined;
60
+ }
61
+ return new Set(input);
62
+ }
40
63
  function normalizeUserScopePattern(pattern) {
41
64
  return normalizePathForMatch(pattern).replace(/^~\//u, "").replace(/^\/+/u, "");
42
65
  }
@@ -63,6 +86,32 @@ function gatherCandidatePatterns(kb) {
63
86
  }
64
87
  return candidates;
65
88
  }
89
+ function isWorkflowCollectionCandidate(reportPath) {
90
+ return /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu.test(normalizePathForMatch(reportPath));
91
+ }
92
+ function isActionCollectionCandidate(reportPath) {
93
+ const fileName = basename(normalizePathForMatch(reportPath)).toLowerCase();
94
+ return fileName === "action.yml" || fileName === "action.yaml";
95
+ }
96
+ function inferCollectionKind(reportPath) {
97
+ if (isWorkflowCollectionCandidate(reportPath)) {
98
+ return "workflows";
99
+ }
100
+ if (isActionCollectionCandidate(reportPath)) {
101
+ return "actions";
102
+ }
103
+ if (isGitHubDependabotPath(reportPath)) {
104
+ return "dependabot";
105
+ }
106
+ return null;
107
+ }
108
+ function matchesCollectionKinds(reportPath, collectKinds) {
109
+ if (!collectKinds) {
110
+ return true;
111
+ }
112
+ const kind = inferCollectionKind(reportPath);
113
+ return kind !== null && collectKinds.has(kind);
114
+ }
66
115
  function isRegularFile(path) {
67
116
  try {
68
117
  return statSync(path).isFile();
@@ -149,6 +198,11 @@ function collectUserScopeWildcardMatches(homeDir, pattern) {
149
198
  }
150
199
  function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, options) {
151
200
  const selected = new Map();
201
+ const includeAll = options.collectModes.has("all");
202
+ const includeProject = includeAll || options.collectModes.has("default") || options.collectModes.has("project");
203
+ const includeUser = includeAll ||
204
+ options.collectModes.has("user") ||
205
+ (options.collectModes.has("default") && options.includeUserScope);
152
206
  const filesByRelativePath = walkedFiles
153
207
  .map((filePath) => ({
154
208
  absolutePath: filePath,
@@ -156,6 +210,9 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
156
210
  }))
157
211
  .filter((entry) => !entry.relativePath.startsWith(".."));
158
212
  for (const file of filesByRelativePath) {
213
+ if (!includeProject) {
214
+ continue;
215
+ }
159
216
  for (const candidate of patterns) {
160
217
  if (candidate.scope !== "project") {
161
218
  continue;
@@ -163,6 +220,9 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
163
220
  if (!wildcardToRegex(candidate.pattern).test(file.relativePath)) {
164
221
  continue;
165
222
  }
223
+ if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
224
+ continue;
225
+ }
166
226
  if (!selected.has(file.relativePath)) {
167
227
  selected.set(file.relativePath, {
168
228
  reportPath: file.relativePath,
@@ -173,8 +233,14 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
173
233
  }
174
234
  }
175
235
  }
176
- if (!options.includeUserScope) {
236
+ if (!includeUser) {
177
237
  for (const file of filesByRelativePath) {
238
+ if (!includeProject) {
239
+ continue;
240
+ }
241
+ if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
242
+ continue;
243
+ }
178
244
  if (selected.has(file.relativePath)) {
179
245
  continue;
180
246
  }
@@ -186,40 +252,54 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
186
252
  }
187
253
  return Array.from(selected.values());
188
254
  }
189
- for (const candidate of patterns) {
190
- if (candidate.scope !== "user") {
191
- continue;
192
- }
193
- const userPattern = normalizeUserScopePattern(candidate.pattern);
194
- if (userPattern.includes("*")) {
195
- for (const match of collectUserScopeWildcardMatches(options.homeDir, userPattern)) {
196
- const reportPath = toUserReportPath(match.relativePath);
197
- if (!selected.has(reportPath)) {
198
- selected.set(reportPath, {
199
- reportPath,
200
- absolutePath: match.absolutePath,
201
- format: candidate.format,
202
- tool: candidate.tool,
203
- });
255
+ if (includeUser) {
256
+ for (const candidate of patterns) {
257
+ if (candidate.scope !== "user") {
258
+ continue;
259
+ }
260
+ const userPattern = normalizeUserScopePattern(candidate.pattern);
261
+ if (userPattern.includes("*")) {
262
+ for (const match of collectUserScopeWildcardMatches(options.homeDir, userPattern)) {
263
+ const reportPath = toUserReportPath(match.relativePath);
264
+ if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
265
+ continue;
266
+ }
267
+ if (!selected.has(reportPath)) {
268
+ selected.set(reportPath, {
269
+ reportPath,
270
+ absolutePath: match.absolutePath,
271
+ format: candidate.format,
272
+ tool: candidate.tool,
273
+ });
274
+ }
204
275
  }
276
+ continue;
277
+ }
278
+ const absolutePath = resolve(options.homeDir, userPattern);
279
+ if (!existsSync(absolutePath) || !isRegularFile(absolutePath)) {
280
+ continue;
281
+ }
282
+ const reportPath = toUserReportPath(userPattern);
283
+ if (!matchesCollectionKinds(reportPath, options.collectKinds)) {
284
+ continue;
285
+ }
286
+ if (!selected.has(reportPath)) {
287
+ selected.set(reportPath, {
288
+ reportPath,
289
+ absolutePath,
290
+ format: candidate.format,
291
+ tool: candidate.tool,
292
+ });
205
293
  }
206
- continue;
207
294
  }
208
- const absolutePath = resolve(options.homeDir, userPattern);
209
- if (!existsSync(absolutePath) || !isRegularFile(absolutePath)) {
295
+ }
296
+ for (const file of filesByRelativePath) {
297
+ if (!includeProject && !includeAll) {
210
298
  continue;
211
299
  }
212
- const reportPath = toUserReportPath(userPattern);
213
- if (!selected.has(reportPath)) {
214
- selected.set(reportPath, {
215
- reportPath,
216
- absolutePath,
217
- format: candidate.format,
218
- tool: candidate.tool,
219
- });
300
+ if (!matchesCollectionKinds(file.relativePath, options.collectKinds)) {
301
+ continue;
220
302
  }
221
- }
222
- for (const file of filesByRelativePath) {
223
303
  if (selected.has(file.relativePath)) {
224
304
  continue;
225
305
  }
@@ -231,7 +311,17 @@ function collectSelectedCandidates(absoluteTarget, walkedFiles, patterns, option
231
311
  }
232
312
  return Array.from(selected.values());
233
313
  }
234
- function mergeExplicitCandidates(selected, explicitCandidates) {
314
+ function normalizeCollectionModes(input) {
315
+ const normalized = new Set();
316
+ for (const mode of input ?? []) {
317
+ normalized.add(mode);
318
+ }
319
+ if (normalized.size === 0) {
320
+ normalized.add("default");
321
+ }
322
+ return normalized;
323
+ }
324
+ function mergeExplicitCandidates(selected, explicitCandidates, collectKinds) {
235
325
  if (!explicitCandidates || explicitCandidates.length === 0) {
236
326
  return selected;
237
327
  }
@@ -240,6 +330,9 @@ function mergeExplicitCandidates(selected, explicitCandidates) {
240
330
  merged.set(candidate.reportPath, candidate);
241
331
  }
242
332
  for (const candidate of explicitCandidates) {
333
+ if (!matchesCollectionKinds(candidate.reportPath, collectKinds)) {
334
+ continue;
335
+ }
243
336
  merged.set(candidate.reportPath, candidate);
244
337
  }
245
338
  return Array.from(merged.values());
@@ -272,11 +365,11 @@ function ensureParsedCandidates(context) {
272
365
  }
273
366
  return context.parsedCandidates;
274
367
  }
275
- function makeParseErrorFinding(filePath, tool, message) {
368
+ function makeParseErrorFinding(filePath, tool, message, strictCollection) {
276
369
  return {
277
370
  rule_id: "parse-error",
278
371
  finding_id: `PARSE_ERROR-${filePath}`,
279
- severity: "LOW",
372
+ severity: strictCollection ? "HIGH" : "LOW",
280
373
  category: "PARSE_ERROR",
281
374
  layer: "L1",
282
375
  file_path: filePath,
@@ -455,6 +548,8 @@ export function discoverDeepScanResources(scanTarget, kbInput, options = {}) {
455
548
  const context = createScanDiscoveryContext(scanTarget, kbInput, {
456
549
  includeUserScope: options.includeUserScope,
457
550
  homeDir: options.homeDir,
551
+ collectModes: options.collectModes,
552
+ collectKinds: options.collectKinds,
458
553
  parseSelected: true,
459
554
  });
460
555
  return discoverDeepScanResourcesFromContext(context);
@@ -468,10 +563,17 @@ export function createScanDiscoveryContext(scanTarget, kbInput, options = {}) {
468
563
  const kb = kbInput ?? loadKnowledgeBase();
469
564
  const patterns = gatherCandidatePatterns(kb);
470
565
  const walked = walkProjectTree(absoluteTarget);
471
- const selected = mergeExplicitCandidates(collectSelectedCandidates(absoluteTarget, walked.files, patterns, {
472
- includeUserScope: options.includeUserScope === true,
473
- homeDir: resolve(options.homeDir ?? homedir()),
474
- }), options.explicitCandidates);
566
+ const collectModes = normalizeCollectionModes(options.collectModes);
567
+ const collectKinds = normalizeCollectionKinds(options.collectKinds);
568
+ const explicitOnly = collectModes.size === 1 && collectModes.has("explicit");
569
+ const selected = mergeExplicitCandidates(explicitOnly
570
+ ? []
571
+ : collectSelectedCandidates(absoluteTarget, walked.files, patterns, {
572
+ includeUserScope: options.includeUserScope === true,
573
+ homeDir: resolve(options.homeDir ?? homedir()),
574
+ collectModes,
575
+ collectKinds,
576
+ }), options.explicitCandidates, collectKinds);
475
577
  return {
476
578
  absoluteTarget,
477
579
  kb,
@@ -494,6 +596,8 @@ export function collectScanSurface(scanTarget, kbInput, options = {}) {
494
596
  const context = createScanDiscoveryContext(scanTarget, kbInput, {
495
597
  includeUserScope: options.includeUserScope === true,
496
598
  homeDir: options.homeDir,
599
+ collectModes: options.collectModes,
600
+ collectKinds: options.collectKinds,
497
601
  });
498
602
  const surface = new Set(context.walked.files);
499
603
  for (const item of context.selected) {
@@ -513,6 +617,8 @@ export async function runScanEngine(input) {
513
617
  const context = input.discoveryContext ??
514
618
  createScanDiscoveryContext(input.scanTarget, input.kb, {
515
619
  includeUserScope: input.config.scan_user_scope === true,
620
+ collectModes: input.config.scan_collection_modes,
621
+ collectKinds: input.config.scan_collection_kinds,
516
622
  homeDir: input.homeDir,
517
623
  parseSelected: true,
518
624
  });
@@ -522,7 +628,7 @@ export async function runScanEngine(input) {
522
628
  const staticFiles = [];
523
629
  for (const item of ensureParsedCandidates(context)) {
524
630
  if (!item.parsed.ok) {
525
- parseErrors.push(makeParseErrorFinding(item.reportPath, item.tool, item.parsed.error));
631
+ parseErrors.push(makeParseErrorFinding(item.reportPath, item.tool, item.parsed.error, input.config.strict_collection === true));
526
632
  continue;
527
633
  }
528
634
  staticFiles.push({
@@ -548,7 +654,7 @@ export async function runScanEngine(input) {
548
654
  executable: (mode & 0o111) !== 0,
549
655
  };
550
656
  });
551
- const report = runStaticPipeline({
657
+ const report = await runStaticPipeline({
552
658
  version: input.version,
553
659
  kbVersion: kb.schemaVersion,
554
660
  scanTarget: input.scanTarget,
@@ -571,6 +677,13 @@ export async function runScanEngine(input) {
571
677
  trustedApiDomains: input.config.trusted_api_domains,
572
678
  unicodeAnalysis: input.config.unicode_analysis,
573
679
  checkIdeSettings: input.config.check_ide_settings,
680
+ rulePackPaths: input.config.rule_pack_paths,
681
+ allowedRules: input.config.allowed_rules,
682
+ skipRules: input.config.skip_rules,
683
+ persona: input.config.persona,
684
+ runtimeMode: input.config.runtime_mode,
685
+ workflowAuditsEnabled: input.config.workflow_audits?.enabled === true,
686
+ rulePolicies: input.config.rules,
574
687
  },
575
688
  });
576
689
  const snapshots = new Map();
@@ -585,7 +698,11 @@ export async function runScanEngine(input) {
585
698
  previousState,
586
699
  });
587
700
  saveScanState(stateResult.nextState, input.scanStatePath);
588
- const findings = [...report.findings, ...parseErrors, ...stateResult.findings];
701
+ const inlineIgnores = collectInlineIgnoreDirectives(staticFiles.map((file) => ({
702
+ filePath: file.filePath,
703
+ textContent: file.textContent,
704
+ })));
705
+ const findings = applyInlineIgnoreDirectives([...report.findings, ...parseErrors, ...stateResult.findings], inlineIgnores);
589
706
  return applyReportSummary({
590
707
  ...report,
591
708
  findings,
@@ -1,6 +1,6 @@
1
1
  export declare const SEVERITIES: readonly ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"];
2
2
  export type Severity = (typeof SEVERITIES)[number];
3
- export declare const FINDING_CATEGORIES: readonly ["ENV_OVERRIDE", "COMMAND_EXEC", "CONSENT_BYPASS", "RULE_INJECTION", "IDE_SETTINGS", "SYMLINK_ESCAPE", "GIT_HOOK", "CONFIG_PRESENT", "PARSE_ERROR", "CONFIG_CHANGE", "NEW_SERVER", "TOXIC_FLOW"];
3
+ export declare const FINDING_CATEGORIES: readonly ["ENV_OVERRIDE", "COMMAND_EXEC", "CONSENT_BYPASS", "RULE_INJECTION", "IDE_SETTINGS", "SYMLINK_ESCAPE", "GIT_HOOK", "CONFIG_PRESENT", "PARSE_ERROR", "CONFIG_CHANGE", "NEW_SERVER", "TOXIC_FLOW", "CI_SUPPLY_CHAIN", "CI_TRIGGER", "CI_PERMISSIONS", "CI_TEMPLATE_INJECTION", "CI_VULNERABLE_ACTION"];
4
4
  export type FindingCategory = (typeof FINDING_CATEGORIES)[number];
5
5
  export type FindingLayer = "L1" | "L2" | "L3";
6
6
  export type FindingConfidence = "HIGH" | "MEDIUM" | "LOW";
@@ -13,6 +13,13 @@ export interface FindingSourceConfig {
13
13
  file_path: string;
14
14
  field?: string;
15
15
  }
16
+ export interface FindingMetadata {
17
+ sources?: string[];
18
+ sinks?: string[];
19
+ referenced_secrets?: string[];
20
+ risk_tags?: string[];
21
+ origin?: string;
22
+ }
16
23
  export interface AffectedLocation {
17
24
  file_path: string;
18
25
  location?: FindingLocation;
@@ -20,6 +27,7 @@ export interface AffectedLocation {
20
27
  export interface Finding {
21
28
  rule_id: string;
22
29
  finding_id: string;
30
+ fingerprint?: string;
23
31
  severity: Severity;
24
32
  category: FindingCategory;
25
33
  layer: FindingLayer;
@@ -34,6 +42,7 @@ export interface Finding {
34
42
  confidence: FindingConfidence;
35
43
  fixable: boolean;
36
44
  remediation_actions: string[];
45
+ metadata?: FindingMetadata | null;
37
46
  evidence?: string | null;
38
47
  observed?: string[] | null;
39
48
  inference?: string | null;
@@ -12,4 +12,9 @@ export const FINDING_CATEGORIES = [
12
12
  "CONFIG_CHANGE",
13
13
  "NEW_SERVER",
14
14
  "TOXIC_FLOW",
15
+ "CI_SUPPLY_CHAIN",
16
+ "CI_TRIGGER",
17
+ "CI_PERMISSIONS",
18
+ "CI_TEMPLATE_INJECTION",
19
+ "CI_VULNERABLE_ACTION",
15
20
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",