codegate-ai 0.8.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 (118) 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/workflow/injection-sinks.json +5 -0
  106. package/dist/layer2-static/workflow/parser.d.ts +3 -0
  107. package/dist/layer2-static/workflow/parser.js +88 -0
  108. package/dist/layer2-static/workflow/types.d.ts +15 -0
  109. package/dist/layer2-static/workflow/types.js +1 -0
  110. package/dist/pipeline.d.ts +1 -1
  111. package/dist/pipeline.js +3 -3
  112. package/dist/reporter/sarif.js +23 -5
  113. package/dist/reporter/terminal.js +12 -0
  114. package/dist/scan.d.ts +7 -1
  115. package/dist/scan.js +154 -40
  116. package/dist/types/finding.d.ts +1 -1
  117. package/dist/types/finding.js +5 -0
  118. package/package.json +1 -1
@@ -0,0 +1,3 @@
1
+ import type { WorkflowFacts } from "./types.js";
2
+ export declare function isGitHubWorkflowPath(path: string): boolean;
3
+ export declare function extractWorkflowFacts(parsed: unknown): WorkflowFacts | null;
@@ -0,0 +1,88 @@
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 normalizeWorkflowPath(value) {
11
+ return value.replaceAll("\\", "/");
12
+ }
13
+ export function isGitHubWorkflowPath(path) {
14
+ return /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/iu.test(normalizeWorkflowPath(path));
15
+ }
16
+ function extractTriggers(value) {
17
+ if (typeof value === "string") {
18
+ return [value];
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.filter((entry) => typeof entry === "string");
22
+ }
23
+ const record = asRecord(value);
24
+ if (!record) {
25
+ return [];
26
+ }
27
+ return Object.keys(record);
28
+ }
29
+ function extractStepFacts(step) {
30
+ const stepRecord = asRecord(step);
31
+ if (!stepRecord) {
32
+ return null;
33
+ }
34
+ const withValues = asRecord(stepRecord.with);
35
+ const withEntries = {};
36
+ if (withValues) {
37
+ for (const [key, value] of Object.entries(withValues)) {
38
+ if (typeof value === "string") {
39
+ withEntries[key] = value;
40
+ }
41
+ }
42
+ }
43
+ const stepFacts = {
44
+ uses: asString(stepRecord.uses),
45
+ run: asString(stepRecord.run),
46
+ with: Object.keys(withEntries).length > 0 ? withEntries : undefined,
47
+ };
48
+ if (!stepFacts.uses && !stepFacts.run) {
49
+ return null;
50
+ }
51
+ return stepFacts;
52
+ }
53
+ function extractJobFacts(id, value) {
54
+ const jobRecord = asRecord(value);
55
+ if (!jobRecord) {
56
+ return null;
57
+ }
58
+ const stepsRaw = Array.isArray(jobRecord.steps) ? jobRecord.steps : [];
59
+ const steps = stepsRaw
60
+ .map((step) => extractStepFacts(step))
61
+ .filter((step) => step !== null);
62
+ return {
63
+ id,
64
+ permissions: jobRecord.permissions,
65
+ steps,
66
+ };
67
+ }
68
+ export function extractWorkflowFacts(parsed) {
69
+ const root = asRecord(parsed);
70
+ if (!root) {
71
+ return null;
72
+ }
73
+ const triggers = extractTriggers(root.on);
74
+ const jobsRecord = asRecord(root.jobs);
75
+ const jobs = jobsRecord
76
+ ? Object.entries(jobsRecord)
77
+ .map(([id, value]) => extractJobFacts(id, value))
78
+ .filter((job) => job !== null)
79
+ : [];
80
+ if (triggers.length === 0 && jobs.length === 0) {
81
+ return null;
82
+ }
83
+ return {
84
+ triggers,
85
+ workflowPermissions: root.permissions,
86
+ jobs,
87
+ };
88
+ }
@@ -0,0 +1,15 @@
1
+ export interface WorkflowStepFacts {
2
+ uses?: string;
3
+ run?: string;
4
+ with?: Record<string, string>;
5
+ }
6
+ export interface WorkflowJobFacts {
7
+ id: string;
8
+ permissions?: unknown;
9
+ steps: WorkflowStepFacts[];
10
+ }
11
+ export interface WorkflowFacts {
12
+ triggers: string[];
13
+ workflowPermissions?: unknown;
14
+ jobs: WorkflowJobFacts[];
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -26,7 +26,7 @@ export interface DeepScanOutcome {
26
26
  status: "skipped_without_consent" | "ok" | "auth_failure" | "timeout" | "network_error" | "command_error";
27
27
  result?: ResourceFetchResult;
28
28
  }
29
- export declare function runStaticPipeline(input: StaticPipelineInput): CodeGateReport;
29
+ export declare function runStaticPipeline(input: StaticPipelineInput): Promise<CodeGateReport>;
30
30
  export declare function layer3OutcomesToFindings(outcomes: DeepScanOutcome[], options?: {
31
31
  unicodeAnalysis?: boolean;
32
32
  }): Finding[];
package/dist/pipeline.js CHANGED
@@ -4,14 +4,14 @@ import { scanToolDescriptions, } from "./layer3-dynamic/tool-description-scanner
4
4
  import { detectToxicFlows } from "./layer3-dynamic/toxic-flow.js";
5
5
  import { applyReportSummary } from "./report-summary.js";
6
6
  import { withFindingFingerprint } from "./report/finding-fingerprint.js";
7
- export function runStaticPipeline(input) {
8
- const findings = runStaticEngine({
7
+ export async function runStaticPipeline(input) {
8
+ const findings = (await runStaticEngine({
9
9
  projectRoot: input.projectRoot,
10
10
  files: input.files,
11
11
  symlinkEscapes: input.symlinkEscapes,
12
12
  hooks: input.hooks,
13
13
  config: input.config,
14
- }).map(withFindingFingerprint);
14
+ })).map(withFindingFingerprint);
15
15
  const report = createEmptyReport({
16
16
  version: input.version,
17
17
  kbVersion: input.kbVersion,
@@ -8,22 +8,39 @@ function toSarifLevel(severity) {
8
8
  return "note";
9
9
  }
10
10
  function findingToLocation(finding) {
11
+ return locationToSarif({
12
+ filePath: finding.file_path,
13
+ location: finding.location,
14
+ });
15
+ }
16
+ function locationToSarif(input) {
11
17
  const region = {};
12
- if (typeof finding.location.line === "number") {
13
- region.startLine = finding.location.line;
18
+ if (typeof input.location?.line === "number") {
19
+ region.startLine = input.location.line;
14
20
  }
15
- if (typeof finding.location.column === "number") {
16
- region.startColumn = finding.location.column;
21
+ if (typeof input.location?.column === "number") {
22
+ region.startColumn = input.location.column;
17
23
  }
18
24
  return {
19
25
  physicalLocation: {
20
26
  artifactLocation: {
21
- uri: finding.file_path,
27
+ uri: input.filePath,
22
28
  },
23
29
  region: Object.keys(region).length > 0 ? region : undefined,
24
30
  },
25
31
  };
26
32
  }
33
+ function findingToRelatedLocations(finding) {
34
+ const related = finding.affected_locations ?? [];
35
+ if (related.length === 0) {
36
+ return undefined;
37
+ }
38
+ const locations = related.map((location) => locationToSarif({
39
+ filePath: location.file_path,
40
+ location: location.location,
41
+ }));
42
+ return locations.length > 0 ? locations : undefined;
43
+ }
27
44
  function buildRules(findings) {
28
45
  const byRuleId = new Map();
29
46
  for (const finding of findings) {
@@ -47,6 +64,7 @@ function findingToResult(finding) {
47
64
  level: toSarifLevel(finding.severity),
48
65
  message: { text: finding.description },
49
66
  locations: [findingToLocation(finding)],
67
+ relatedLocations: findingToRelatedLocations(finding),
50
68
  properties: {
51
69
  finding_id: finding.finding_id,
52
70
  fingerprint: finding.fingerprint ?? null,
@@ -84,6 +84,18 @@ function appendFinding(lines, report, options, finding) {
84
84
  if (formattedLocation) {
85
85
  lines.push(` Location: ${formattedLocation}`);
86
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
+ }
87
99
  if (finding.cve) {
88
100
  lines.push(` CVE: ${finding.cve}`);
89
101
  }
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,
@@ -574,6 +680,10 @@ export async function runScanEngine(input) {
574
680
  rulePackPaths: input.config.rule_pack_paths,
575
681
  allowedRules: input.config.allowed_rules,
576
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,
577
687
  },
578
688
  });
579
689
  const snapshots = new Map();
@@ -588,7 +698,11 @@ export async function runScanEngine(input) {
588
698
  previousState,
589
699
  });
590
700
  saveScanState(stateResult.nextState, input.scanStatePath);
591
- 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);
592
706
  return applyReportSummary({
593
707
  ...report,
594
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";
@@ -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.8.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",