codegate-ai 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +109 -40
  2. package/dist/cli.js +85 -7
  3. package/dist/commands/scan-command.d.ts +6 -1
  4. package/dist/commands/scan-command.js +1 -1
  5. package/dist/commands/scan-content-command.d.ts +16 -0
  6. package/dist/commands/scan-content-command.js +65 -0
  7. package/dist/config/inline-ignore.d.ts +11 -0
  8. package/dist/config/inline-ignore.js +57 -0
  9. package/dist/config/suppression-policy.d.ts +22 -0
  10. package/dist/config/suppression-policy.js +146 -0
  11. package/dist/config.d.ts +23 -0
  12. package/dist/config.js +188 -3
  13. package/dist/layer2-static/action/parser.d.ts +3 -0
  14. package/dist/layer2-static/action/parser.js +165 -0
  15. package/dist/layer2-static/action/types.d.ts +39 -0
  16. package/dist/layer2-static/action/types.js +1 -0
  17. package/dist/layer2-static/advisories/agent-components.json +62 -0
  18. package/dist/layer2-static/advisories/cache.d.ts +6 -0
  19. package/dist/layer2-static/advisories/cache.js +44 -0
  20. package/dist/layer2-static/advisories/cache.ts +62 -0
  21. package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
  22. package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
  23. package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
  24. package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
  25. package/dist/layer2-static/audits/registry.d.ts +3 -0
  26. package/dist/layer2-static/audits/registry.js +29 -0
  27. package/dist/layer2-static/audits/types.d.ts +12 -0
  28. package/dist/layer2-static/audits/types.js +1 -0
  29. package/dist/layer2-static/dependabot/parser.d.ts +3 -0
  30. package/dist/layer2-static/dependabot/parser.js +189 -0
  31. package/dist/layer2-static/dependabot/types.d.ts +50 -0
  32. package/dist/layer2-static/dependabot/types.js +1 -0
  33. package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
  34. package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
  35. package/dist/layer2-static/detectors/command-exec.js +6 -0
  36. package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
  37. package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
  38. package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
  39. package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
  40. package/dist/layer2-static/detectors/rule-file.js +5 -0
  41. package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
  42. package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
  43. package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
  44. package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
  45. package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
  46. package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
  47. package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
  48. package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
  49. package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
  50. package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
  51. package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
  52. package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
  53. package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
  54. package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
  55. package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
  56. package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
  57. package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
  58. package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
  59. package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
  60. package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
  61. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
  62. package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
  63. package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
  64. package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
  65. package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
  66. package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
  67. package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
  68. package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
  69. package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
  70. package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
  71. package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
  72. package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
  73. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
  74. package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
  75. package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
  76. package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
  77. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
  78. package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
  79. package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
  80. package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
  81. package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
  82. package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
  83. package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
  84. package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
  85. package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
  86. package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
  87. package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
  88. package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
  89. package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
  90. package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
  91. package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
  92. package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
  93. package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
  94. package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
  95. package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
  96. package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
  97. package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
  98. package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
  99. package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
  100. package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
  101. package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
  102. package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
  103. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
  104. package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
  105. package/dist/layer2-static/engine.d.ts +13 -2
  106. package/dist/layer2-static/engine.js +578 -41
  107. package/dist/layer2-static/github/cache.d.ts +8 -0
  108. package/dist/layer2-static/github/cache.js +41 -0
  109. package/dist/layer2-static/github/client.d.ts +17 -0
  110. package/dist/layer2-static/github/client.js +43 -0
  111. package/dist/layer2-static/rule-engine.d.ts +1 -1
  112. package/dist/layer2-static/rule-engine.js +1 -13
  113. package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
  114. package/dist/layer2-static/rule-pack-loader.js +187 -0
  115. package/dist/layer2-static/workflow/injection-sinks.json +5 -0
  116. package/dist/layer2-static/workflow/parser.d.ts +3 -0
  117. package/dist/layer2-static/workflow/parser.js +88 -0
  118. package/dist/layer2-static/workflow/types.d.ts +15 -0
  119. package/dist/layer2-static/workflow/types.js +1 -0
  120. package/dist/layer3-dynamic/toxic-flow.js +6 -0
  121. package/dist/pipeline.d.ts +1 -1
  122. package/dist/pipeline.js +11 -10
  123. package/dist/report/finding-fingerprint.d.ts +5 -0
  124. package/dist/report/finding-fingerprint.js +47 -0
  125. package/dist/reporter/markdown.js +25 -3
  126. package/dist/reporter/sarif.js +25 -5
  127. package/dist/reporter/terminal.js +37 -0
  128. package/dist/scan-target/fetch-plan.d.ts +8 -0
  129. package/dist/scan-target/fetch-plan.js +30 -0
  130. package/dist/scan-target/staging.js +60 -5
  131. package/dist/scan.d.ts +7 -1
  132. package/dist/scan.js +157 -40
  133. package/dist/types/finding.d.ts +10 -1
  134. package/dist/types/finding.js +5 -0
  135. package/package.json +1 -1
@@ -0,0 +1,69 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ const HASH_PINNED_REF_RE = /^[a-f0-9]{40}$/iu;
4
+ function parseRepositoryUses(value) {
5
+ const trimmed = value.trim();
6
+ if (trimmed.startsWith("./") || trimmed.startsWith("docker://")) {
7
+ return null;
8
+ }
9
+ const atIndex = trimmed.lastIndexOf("@");
10
+ if (atIndex < 0) {
11
+ return null;
12
+ }
13
+ const slug = trimmed.slice(0, atIndex).trim().replace(/\/+$/u, "").toLowerCase();
14
+ const ref = trimmed.slice(atIndex + 1).trim();
15
+ if (slug.length === 0 || !slug.includes("/") || ref.length === 0) {
16
+ return null;
17
+ }
18
+ return { slug, ref };
19
+ }
20
+ function isHashPinned(ref) {
21
+ return HASH_PINNED_REF_RE.test(ref.trim());
22
+ }
23
+ export function detectWorkflowRefConfusion(input) {
24
+ if (!isGitHubWorkflowPath(input.filePath)) {
25
+ return [];
26
+ }
27
+ const facts = extractWorkflowFacts(input.parsed);
28
+ if (!facts) {
29
+ return [];
30
+ }
31
+ const findings = [];
32
+ for (const [jobIndex, job] of facts.jobs.entries()) {
33
+ for (const [stepIndex, step] of job.steps.entries()) {
34
+ const uses = step.uses?.trim();
35
+ if (!uses) {
36
+ continue;
37
+ }
38
+ const parsedUses = parseRepositoryUses(uses);
39
+ if (!parsedUses || isHashPinned(parsedUses.ref)) {
40
+ continue;
41
+ }
42
+ const evidence = buildFindingEvidence({
43
+ textContent: input.textContent,
44
+ searchTerms: [uses],
45
+ fallbackValue: `uses: ${uses}`,
46
+ });
47
+ findings.push({
48
+ rule_id: "workflow-ref-confusion",
49
+ finding_id: `WORKFLOW_REF_CONFUSION-${input.filePath}-${jobIndex}-${stepIndex}`,
50
+ severity: "HIGH",
51
+ category: "CI_VULNERABLE_ACTION",
52
+ layer: "L2",
53
+ file_path: input.filePath,
54
+ location: { field: `jobs.${job.id}.steps[${stepIndex}].uses` },
55
+ description: "Workflow action is pinned to a symbolic ref instead of an immutable commit hash",
56
+ affected_tools: ["github-actions"],
57
+ cve: null,
58
+ owasp: ["ASI02"],
59
+ cwe: "CWE-829",
60
+ confidence: "HIGH",
61
+ fixable: false,
62
+ remediation_actions: ["Pin external actions to a full commit SHA"],
63
+ evidence: evidence?.evidence ?? null,
64
+ suppressed: false,
65
+ });
66
+ }
67
+ }
68
+ return findings;
69
+ }
@@ -0,0 +1,9 @@
1
+ import type { RuntimeMode } from "../../config.js";
2
+ import type { Finding } from "../../types/finding.js";
3
+ export interface WorkflowRefVersionMismatchInput {
4
+ filePath: string;
5
+ parsed: unknown;
6
+ textContent: string;
7
+ runtimeMode?: RuntimeMode;
8
+ }
9
+ export declare function detectWorkflowRefVersionMismatch(input: WorkflowRefVersionMismatchInput): Promise<Finding[]>;
@@ -0,0 +1,179 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ const VERSION_COMMENT_PATTERNS = [
4
+ /#\s*tag\s*=\s*(v\d+(?:\.\d+)*(?:\.\d+)?)/iu,
5
+ /#\s*(v\d+(?:\.\d+)*(?:\.\d+)?)/iu,
6
+ /#\s*tag\s*=\s*(\d+(?:\.\d+)*(?:\.\d+)?)/iu,
7
+ /#\s*(?:version|ver)\s*[:=]\s*(v?\d+(?:\.\d+)*(?:\.\d+)?)/iu,
8
+ ];
9
+ const GITHUB_API_HEADERS = {
10
+ Accept: "application/vnd.github+json",
11
+ };
12
+ const MAX_TAG_DEPTH = 8;
13
+ function splitOwnerRepo(slug) {
14
+ const firstSlash = slug.indexOf("/");
15
+ if (firstSlash < 0) {
16
+ return null;
17
+ }
18
+ const owner = slug.slice(0, firstSlash);
19
+ const repo = slug.slice(firstSlash + 1).split("/")[0];
20
+ if (!owner || !repo) {
21
+ return null;
22
+ }
23
+ return { owner, repo };
24
+ }
25
+ function isPinnedToCommit(ref) {
26
+ return /^[a-f0-9]{40}$/iu.test(ref.trim());
27
+ }
28
+ function parseRepositoryUses(value) {
29
+ const trimmed = value.trim();
30
+ if (trimmed.startsWith("./") || trimmed.startsWith("docker://")) {
31
+ return null;
32
+ }
33
+ const atIndex = trimmed.lastIndexOf("@");
34
+ if (atIndex < 0) {
35
+ return null;
36
+ }
37
+ const slug = trimmed.slice(0, atIndex).trim().replace(/\/+$/u, "").toLowerCase();
38
+ const ref = trimmed.slice(atIndex + 1).trim();
39
+ if (!slug.includes("/") || ref.length === 0) {
40
+ return null;
41
+ }
42
+ return { slug, ref };
43
+ }
44
+ function extractVersionComment(line) {
45
+ for (const pattern of VERSION_COMMENT_PATTERNS) {
46
+ const match = line.match(pattern);
47
+ if (match?.[1]) {
48
+ return match[1];
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+ function parseUsesLine(line, lineNumber) {
54
+ const match = line.match(/^\s*(?:-\s*)?uses:\s*([^#]+?)(?:\s*#\s*(.+))?\s*$/iu);
55
+ if (!match?.[1]) {
56
+ return null;
57
+ }
58
+ const uses = match[1].trim();
59
+ const versionComment = extractVersionComment(line);
60
+ if (!versionComment) {
61
+ return null;
62
+ }
63
+ const usesColumn = line.indexOf("uses:");
64
+ return {
65
+ lineNumber,
66
+ column: usesColumn >= 0 ? usesColumn + 1 : 1,
67
+ rawLine: line,
68
+ uses,
69
+ versionComment,
70
+ };
71
+ }
72
+ async function fetchGitHubJson(url) {
73
+ try {
74
+ const response = await fetch(url, {
75
+ headers: GITHUB_API_HEADERS,
76
+ });
77
+ if (!response.ok) {
78
+ return null;
79
+ }
80
+ return (await response.json());
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ async function resolveTagCommitSha(owner, repo, tag) {
87
+ const normalizedTag = tag.trim();
88
+ if (normalizedTag.length === 0) {
89
+ return null;
90
+ }
91
+ const refUrl = `https://api.github.com/repos/${owner}/${repo}/git/ref/tags/${encodeURIComponent(normalizedTag)}`;
92
+ const ref = (await fetchGitHubJson(refUrl));
93
+ if (!ref?.object?.sha || !ref.object.type) {
94
+ return null;
95
+ }
96
+ if (ref.object.type === "commit") {
97
+ return ref.object.sha;
98
+ }
99
+ if (ref.object.type !== "tag") {
100
+ return null;
101
+ }
102
+ const seenObjects = new Set([ref.object.sha]);
103
+ let tagObjectSha = ref.object.sha;
104
+ for (let depth = 0; depth < MAX_TAG_DEPTH; depth += 1) {
105
+ const tagUrl = `https://api.github.com/repos/${owner}/${repo}/git/tags/${tagObjectSha}`;
106
+ const tagObject = (await fetchGitHubJson(tagUrl));
107
+ if (!tagObject?.object?.sha || !tagObject.object.type) {
108
+ return null;
109
+ }
110
+ if (tagObject.object.type === "commit") {
111
+ return tagObject.object.sha;
112
+ }
113
+ if (tagObject.object.type !== "tag" || seenObjects.has(tagObject.object.sha)) {
114
+ return null;
115
+ }
116
+ seenObjects.add(tagObject.object.sha);
117
+ tagObjectSha = tagObject.object.sha;
118
+ }
119
+ return null;
120
+ }
121
+ export async function detectWorkflowRefVersionMismatch(input) {
122
+ if (!isGitHubWorkflowPath(input.filePath)) {
123
+ return [];
124
+ }
125
+ if (!extractWorkflowFacts(input.parsed)) {
126
+ return [];
127
+ }
128
+ const mode = input.runtimeMode ?? "offline";
129
+ if (mode !== "online") {
130
+ return [];
131
+ }
132
+ const findings = [];
133
+ const lines = input.textContent.split(/\r?\n/u);
134
+ for (const [index, line] of lines.entries()) {
135
+ const parsedLine = parseUsesLine(line, index + 1);
136
+ if (!parsedLine) {
137
+ continue;
138
+ }
139
+ const parsedUses = parseRepositoryUses(parsedLine.uses);
140
+ if (!parsedUses || !isPinnedToCommit(parsedUses.ref)) {
141
+ continue;
142
+ }
143
+ const ownerRepo = splitOwnerRepo(parsedUses.slug);
144
+ if (!ownerRepo) {
145
+ continue;
146
+ }
147
+ const matchedCommit = await resolveTagCommitSha(ownerRepo.owner, ownerRepo.repo, parsedLine.versionComment);
148
+ if (!matchedCommit || matchedCommit === parsedUses.ref) {
149
+ continue;
150
+ }
151
+ const evidence = buildFindingEvidence({
152
+ textContent: input.textContent,
153
+ searchTerms: [parsedLine.rawLine, parsedLine.versionComment, parsedUses.ref],
154
+ fallbackValue: parsedLine.rawLine.trim(),
155
+ });
156
+ findings.push({
157
+ rule_id: "workflow-ref-version-mismatch",
158
+ finding_id: `WORKFLOW_REF_VERSION_MISMATCH-${input.filePath}-${index + 1}`,
159
+ severity: "MEDIUM",
160
+ category: "CI_SUPPLY_CHAIN",
161
+ layer: "L2",
162
+ file_path: input.filePath,
163
+ location: { line: parsedLine.lineNumber, column: parsedLine.column },
164
+ description: "Hash-pinned action commit does not match its version comment tag",
165
+ affected_tools: ["github-actions"],
166
+ cve: null,
167
+ owasp: ["ASI02"],
168
+ cwe: "CWE-829",
169
+ confidence: "HIGH",
170
+ fixable: false,
171
+ remediation_actions: [
172
+ "Update the version comment to match the pinned commit or repin the action to the intended release tag",
173
+ ],
174
+ evidence: evidence?.evidence ?? parsedLine.rawLine.trim(),
175
+ suppressed: false,
176
+ });
177
+ }
178
+ return findings;
179
+ }
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowSecretsInheritInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectWorkflowSecretsInherit(input: WorkflowSecretsInheritInput): Finding[];
@@ -0,0 +1,59 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ function asRecord(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
+ return null;
6
+ }
7
+ return value;
8
+ }
9
+ function isReusableWorkflowCall(uses) {
10
+ return typeof uses === "string" && uses.includes("/.github/workflows/");
11
+ }
12
+ function isInheritSecrets(value) {
13
+ return typeof value === "string" && value.trim().toLowerCase() === "inherit";
14
+ }
15
+ export function detectWorkflowSecretsInherit(input) {
16
+ if (!isGitHubWorkflowPath(input.filePath)) {
17
+ return [];
18
+ }
19
+ const facts = extractWorkflowFacts(input.parsed);
20
+ const root = asRecord(input.parsed);
21
+ const jobsRecord = asRecord(root?.jobs);
22
+ if (!facts || !jobsRecord) {
23
+ return [];
24
+ }
25
+ const findings = [];
26
+ for (const job of facts.jobs) {
27
+ const jobRecord = asRecord(jobsRecord[job.id]);
28
+ if (!jobRecord ||
29
+ !isReusableWorkflowCall(jobRecord.uses) ||
30
+ !isInheritSecrets(jobRecord.secrets)) {
31
+ continue;
32
+ }
33
+ const evidence = buildFindingEvidence({
34
+ textContent: input.textContent,
35
+ searchTerms: ["secrets: inherit", "inherit"],
36
+ fallbackValue: "secrets: inherit",
37
+ });
38
+ findings.push({
39
+ rule_id: "workflow-secrets-inherit",
40
+ finding_id: `WORKFLOW_SECRETS_INHERIT-${input.filePath}-${job.id}`,
41
+ severity: "HIGH",
42
+ category: "CI_PERMISSIONS",
43
+ layer: "L2",
44
+ file_path: input.filePath,
45
+ location: { field: `jobs.${job.id}.secrets` },
46
+ description: "Reusable workflow call inherits all repository secrets",
47
+ affected_tools: ["github-actions"],
48
+ cve: null,
49
+ owasp: ["ASI02"],
50
+ cwe: "CWE-200",
51
+ confidence: "HIGH",
52
+ fixable: false,
53
+ remediation_actions: ["Pass only the specific secrets required by the reusable workflow"],
54
+ evidence: evidence?.evidence ?? null,
55
+ suppressed: false,
56
+ });
57
+ }
58
+ return findings;
59
+ }
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowSecretsOutsideEnvInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectWorkflowSecretsOutsideEnv(input: WorkflowSecretsOutsideEnvInput): Finding[];
@@ -0,0 +1,99 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ const DIRECT_SECRET_REFERENCE = /\bsecrets\.(?!GITHUB_TOKEN\b)[A-Za-z0-9_]+\b/iu;
4
+ const BRACKET_SECRET_REFERENCE = /\bsecrets\s*\[\s*['"][^'"\r\n]+['"]\s*\]/iu;
5
+ function asRecord(value) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
7
+ return null;
8
+ }
9
+ return value;
10
+ }
11
+ function appendPath(base, segment) {
12
+ if (typeof segment === "number") {
13
+ return `${base}[${segment}]`;
14
+ }
15
+ return base.length > 0 ? `${base}.${segment}` : segment;
16
+ }
17
+ function findSecretReference(value, path) {
18
+ if (typeof value === "string") {
19
+ return DIRECT_SECRET_REFERENCE.test(value) || BRACKET_SECRET_REFERENCE.test(value)
20
+ ? { path, value }
21
+ : null;
22
+ }
23
+ if (Array.isArray(value)) {
24
+ for (let index = 0; index < value.length; index += 1) {
25
+ const found = findSecretReference(value[index], appendPath(path, index));
26
+ if (found) {
27
+ return found;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ const record = asRecord(value);
33
+ if (!record) {
34
+ return null;
35
+ }
36
+ for (const [key, child] of Object.entries(record)) {
37
+ const found = findSecretReference(child, appendPath(path, key));
38
+ if (found) {
39
+ return found;
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+ function hasDedicatedEnvironment(job) {
45
+ const environment = job.environment;
46
+ if (typeof environment === "string") {
47
+ return environment.trim().length > 0;
48
+ }
49
+ return environment !== undefined && environment !== null;
50
+ }
51
+ export function detectWorkflowSecretsOutsideEnv(input) {
52
+ if (!isGitHubWorkflowPath(input.filePath)) {
53
+ return [];
54
+ }
55
+ const facts = extractWorkflowFacts(input.parsed);
56
+ const root = asRecord(input.parsed);
57
+ const jobsRecord = asRecord(root?.jobs);
58
+ if (!facts || !jobsRecord) {
59
+ return [];
60
+ }
61
+ const findings = [];
62
+ for (const job of facts.jobs) {
63
+ const jobRecord = asRecord(jobsRecord[job.id]);
64
+ if (!jobRecord || hasDedicatedEnvironment(jobRecord)) {
65
+ continue;
66
+ }
67
+ const found = findSecretReference(jobRecord, `jobs.${job.id}`);
68
+ if (!found) {
69
+ continue;
70
+ }
71
+ const evidence = buildFindingEvidence({
72
+ textContent: input.textContent,
73
+ searchTerms: [found.value, "secrets."],
74
+ fallbackValue: found.value,
75
+ });
76
+ findings.push({
77
+ rule_id: "workflow-secrets-outside-env",
78
+ finding_id: `WORKFLOW_SECRETS_OUTSIDE_ENV-${input.filePath}-${job.id}`,
79
+ severity: "HIGH",
80
+ category: "CI_PERMISSIONS",
81
+ layer: "L2",
82
+ file_path: input.filePath,
83
+ location: { field: found.path },
84
+ description: "Job references workflow secrets without a dedicated environment",
85
+ affected_tools: ["github-actions"],
86
+ cve: null,
87
+ owasp: ["ASI02"],
88
+ cwe: "CWE-522",
89
+ confidence: "HIGH",
90
+ fixable: false,
91
+ remediation_actions: [
92
+ "Use a dedicated environment for secrets-bound jobs or reduce the scope of secret exposure",
93
+ ],
94
+ evidence: evidence?.evidence ?? null,
95
+ suppressed: false,
96
+ });
97
+ }
98
+ return findings;
99
+ }
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowSelfHostedRunnerInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectWorkflowSelfHostedRunner(input: WorkflowSelfHostedRunnerInput): Finding[];
@@ -0,0 +1,67 @@
1
+ import { buildFindingEvidence } from "../evidence.js";
2
+ import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
3
+ function asRecord(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5
+ return null;
6
+ }
7
+ return value;
8
+ }
9
+ function normalizeRunsOn(value) {
10
+ if (typeof value === "string") {
11
+ return [value.trim()];
12
+ }
13
+ if (!Array.isArray(value)) {
14
+ return [];
15
+ }
16
+ return value
17
+ .filter((entry) => typeof entry === "string")
18
+ .map((entry) => entry.trim());
19
+ }
20
+ function isSelfHostedRunner(value) {
21
+ return normalizeRunsOn(value).some((entry) => entry.toLowerCase() === "self-hosted");
22
+ }
23
+ export function detectWorkflowSelfHostedRunner(input) {
24
+ if (!isGitHubWorkflowPath(input.filePath)) {
25
+ return [];
26
+ }
27
+ const facts = extractWorkflowFacts(input.parsed);
28
+ const root = asRecord(input.parsed);
29
+ const jobsRecord = asRecord(root?.jobs);
30
+ if (!facts || !jobsRecord) {
31
+ return [];
32
+ }
33
+ const findings = [];
34
+ for (const job of facts.jobs) {
35
+ const jobRecord = jobsRecord[job.id];
36
+ if (!isSelfHostedRunner(asRecord(jobRecord)?.["runs-on"])) {
37
+ continue;
38
+ }
39
+ const evidence = buildFindingEvidence({
40
+ textContent: input.textContent,
41
+ searchTerms: ["self-hosted"],
42
+ fallbackValue: "runs-on: self-hosted",
43
+ });
44
+ findings.push({
45
+ rule_id: "workflow-self-hosted-runner",
46
+ finding_id: `WORKFLOW_SELF_HOSTED_RUNNER-${input.filePath}-${job.id}`,
47
+ severity: "MEDIUM",
48
+ category: "CI_PERMISSIONS",
49
+ layer: "L2",
50
+ file_path: input.filePath,
51
+ location: { field: `jobs.${job.id}.runs-on` },
52
+ description: "Job uses a self-hosted runner",
53
+ affected_tools: ["github-actions"],
54
+ cve: null,
55
+ owasp: ["ASI02"],
56
+ cwe: "CWE-732",
57
+ confidence: "MEDIUM",
58
+ fixable: false,
59
+ remediation_actions: [
60
+ "Prefer GitHub-hosted runners unless the workflow requires a hardened self-hosted trust boundary",
61
+ ],
62
+ evidence: evidence?.evidence ?? null,
63
+ suppressed: false,
64
+ });
65
+ }
66
+ return findings;
67
+ }
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/finding.js";
2
+ export interface WorkflowStaleActionRefsInput {
3
+ filePath: string;
4
+ parsed: unknown;
5
+ textContent: string;
6
+ }
7
+ export declare function detectWorkflowStaleActionRefs(input: WorkflowStaleActionRefsInput): Promise<Finding[]>;