codegate-ai 0.9.0 → 0.10.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 (28) hide show
  1. package/dist/layer2-static/detectors/dependabot-auto-merge.d.ts +7 -0
  2. package/dist/layer2-static/detectors/dependabot-auto-merge.js +118 -0
  3. package/dist/layer2-static/detectors/workflow-artifact-trust-chain.d.ts +7 -0
  4. package/dist/layer2-static/detectors/workflow-artifact-trust-chain.js +89 -0
  5. package/dist/layer2-static/detectors/workflow-call-boundary.d.ts +7 -0
  6. package/dist/layer2-static/detectors/workflow-call-boundary.js +92 -0
  7. package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.d.ts +7 -0
  8. package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.js +149 -0
  9. package/dist/layer2-static/detectors/workflow-local-action-mutation.d.ts +7 -0
  10. package/dist/layer2-static/detectors/workflow-local-action-mutation.js +125 -0
  11. package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.d.ts +7 -0
  12. package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.js +166 -0
  13. package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.d.ts +7 -0
  14. package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.js +99 -0
  15. package/dist/layer2-static/detectors/workflow-secret-exfiltration.d.ts +8 -0
  16. package/dist/layer2-static/detectors/workflow-secret-exfiltration.js +97 -0
  17. package/dist/layer2-static/engine.js +89 -0
  18. package/dist/layer2-static/rules/claude-code.json +16 -0
  19. package/dist/layer2-static/rules/codex.json +16 -0
  20. package/dist/layer2-static/rules/common.json +15 -0
  21. package/dist/layer2-static/rules/copilot.json +15 -0
  22. package/dist/layer2-static/rules/cursor.json +15 -0
  23. package/dist/layer2-static/rules/opencode.json +15 -0
  24. package/dist/layer2-static/workflow/analysis.d.ts +24 -0
  25. package/dist/layer2-static/workflow/analysis.js +201 -0
  26. package/dist/layer2-static/workflow/parser.js +30 -10
  27. package/dist/layer2-static/workflow/types.d.ts +6 -0
  28. package/package.json +1 -1
@@ -0,0 +1,201 @@
1
+ const UNTRUSTED_TRIGGER_SET = new Set([
2
+ "pull_request",
3
+ "pull_request_target",
4
+ "workflow_run",
5
+ "issue_comment",
6
+ "pull_request_review_comment",
7
+ "discussion_comment",
8
+ ]);
9
+ const BOT_ONLY_CONDITION_PATTERNS = [
10
+ /github\.actor\s*==\s*['"]dependabot\[bot\]['"]/iu,
11
+ /github\.actor\s*==\s*['"]github-actions\[bot\]['"]/iu,
12
+ /github\.event\.pull_request\.head\.repo\.fork\s*==\s*false/iu,
13
+ ];
14
+ const UPLOAD_ARTIFACT_ACTIONS = new Set([
15
+ "actions/upload-artifact",
16
+ "actions/upload-artifact/merge",
17
+ ]);
18
+ const DOWNLOAD_ARTIFACT_ACTIONS = new Set(["actions/download-artifact"]);
19
+ function asRecord(value) {
20
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
21
+ return null;
22
+ }
23
+ return value;
24
+ }
25
+ function normalizeUses(uses) {
26
+ if (!uses) {
27
+ return null;
28
+ }
29
+ const trimmed = uses.trim().toLowerCase();
30
+ if (trimmed.length === 0) {
31
+ return null;
32
+ }
33
+ const atIndex = trimmed.indexOf("@");
34
+ if (atIndex === -1) {
35
+ return trimmed;
36
+ }
37
+ return trimmed.slice(0, atIndex);
38
+ }
39
+ function isWorkflowTriggerUntrusted(trigger) {
40
+ return UNTRUSTED_TRIGGER_SET.has(trigger.trim().toLowerCase());
41
+ }
42
+ function isUntrustedRestrictedCondition(condition) {
43
+ if (!condition) {
44
+ return false;
45
+ }
46
+ return BOT_ONLY_CONDITION_PATTERNS.some((pattern) => pattern.test(condition));
47
+ }
48
+ function normalizeArtifactName(value) {
49
+ if (!value) {
50
+ return null;
51
+ }
52
+ const normalized = value.trim();
53
+ return normalized.length > 0 ? normalized : null;
54
+ }
55
+ export function buildWorkflowNeedsGraph(facts) {
56
+ return new Map(facts.jobs.map((job) => [job.id, [...job.needs]]));
57
+ }
58
+ export function collectTransitiveDependencies(facts, seedJobIds) {
59
+ const graph = buildWorkflowNeedsGraph(facts);
60
+ const visited = new Set();
61
+ const queue = [...seedJobIds];
62
+ while (queue.length > 0) {
63
+ const current = queue.shift();
64
+ if (!current) {
65
+ continue;
66
+ }
67
+ const dependencies = graph.get(current) ?? [];
68
+ for (const dependency of dependencies) {
69
+ if (visited.has(dependency)) {
70
+ continue;
71
+ }
72
+ visited.add(dependency);
73
+ queue.push(dependency);
74
+ }
75
+ }
76
+ return visited;
77
+ }
78
+ export function collectTransitiveDependents(facts, seedJobIds) {
79
+ const reverseGraph = new Map();
80
+ for (const job of facts.jobs) {
81
+ for (const dependency of job.needs) {
82
+ const dependents = reverseGraph.get(dependency) ?? [];
83
+ dependents.push(job.id);
84
+ reverseGraph.set(dependency, dependents);
85
+ }
86
+ }
87
+ const visited = new Set();
88
+ const queue = [...seedJobIds];
89
+ while (queue.length > 0) {
90
+ const current = queue.shift();
91
+ if (!current) {
92
+ continue;
93
+ }
94
+ const dependents = reverseGraph.get(current) ?? [];
95
+ for (const dependent of dependents) {
96
+ if (visited.has(dependent)) {
97
+ continue;
98
+ }
99
+ visited.add(dependent);
100
+ queue.push(dependent);
101
+ }
102
+ }
103
+ return visited;
104
+ }
105
+ export function collectArtifactTransferEdges(facts) {
106
+ const producersByArtifact = new Map();
107
+ const edges = [];
108
+ const dedupe = new Set();
109
+ for (const job of facts.jobs) {
110
+ for (const [stepIndex, step] of job.steps.entries()) {
111
+ const normalizedUses = normalizeUses(step.uses);
112
+ if (!normalizedUses || !UPLOAD_ARTIFACT_ACTIONS.has(normalizedUses)) {
113
+ continue;
114
+ }
115
+ const artifactName = normalizeArtifactName(step.with?.name) ?? "__unnamed__";
116
+ const producers = producersByArtifact.get(artifactName) ?? [];
117
+ producers.push({ jobId: job.id, stepIndex });
118
+ producersByArtifact.set(artifactName, producers);
119
+ }
120
+ }
121
+ for (const job of facts.jobs) {
122
+ for (const [stepIndex, step] of job.steps.entries()) {
123
+ const normalizedUses = normalizeUses(step.uses);
124
+ if (!normalizedUses || !DOWNLOAD_ARTIFACT_ACTIONS.has(normalizedUses)) {
125
+ continue;
126
+ }
127
+ const requestedName = normalizeArtifactName(step.with?.name);
128
+ const consumerDownloadsAll = !requestedName;
129
+ const artifactNames = requestedName
130
+ ? [requestedName]
131
+ : Array.from(producersByArtifact.keys());
132
+ for (const artifactName of artifactNames) {
133
+ const producers = producersByArtifact.get(artifactName) ?? [];
134
+ for (const producer of producers) {
135
+ const key = [
136
+ artifactName,
137
+ producer.jobId,
138
+ producer.stepIndex,
139
+ job.id,
140
+ stepIndex,
141
+ consumerDownloadsAll ? "all" : "named",
142
+ ].join("|");
143
+ if (dedupe.has(key)) {
144
+ continue;
145
+ }
146
+ dedupe.add(key);
147
+ edges.push({
148
+ artifactName,
149
+ producerJobId: producer.jobId,
150
+ producerStepIndex: producer.stepIndex,
151
+ consumerJobId: job.id,
152
+ consumerStepIndex: stepIndex,
153
+ consumerDownloadsAll,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ }
159
+ return edges;
160
+ }
161
+ export function collectUntrustedReachableJobIds(facts) {
162
+ const hasUntrustedTrigger = facts.triggers.some((trigger) => isWorkflowTriggerUntrusted(trigger));
163
+ if (!hasUntrustedTrigger) {
164
+ return new Set();
165
+ }
166
+ return new Set(facts.jobs.filter((job) => !isUntrustedRestrictedCondition(job.if)).map((job) => job.id));
167
+ }
168
+ export function extractWorkflowCallBoundaryContext(parsed, facts) {
169
+ const root = asRecord(parsed);
170
+ const onRecord = root ? asRecord(root.on) : null;
171
+ const workflowCall = onRecord ? asRecord(onRecord.workflow_call) : null;
172
+ const inputsRecord = workflowCall ? asRecord(workflowCall.inputs) : null;
173
+ const declaredInputKeys = inputsRecord ? Object.keys(inputsRecord) : [];
174
+ const requiredInputKeys = inputsRecord
175
+ ? Object.entries(inputsRecord)
176
+ .filter(([, value]) => asRecord(value)?.required === true)
177
+ .map(([key]) => key)
178
+ : [];
179
+ const secretsRecord = workflowCall ? asRecord(workflowCall.secrets) : null;
180
+ const declaredSecretKeys = secretsRecord ? Object.keys(secretsRecord) : [];
181
+ const requiredSecretKeys = secretsRecord
182
+ ? Object.entries(secretsRecord)
183
+ .filter(([, value]) => asRecord(value)?.required === true)
184
+ .map(([key]) => key)
185
+ : [];
186
+ const jobsWithInheritedSecrets = facts.jobs
187
+ .filter((job) => typeof job.secrets === "string" && job.secrets.trim().toLowerCase() === "inherit")
188
+ .map((job) => job.id);
189
+ const jobsCallingReusableWorkflow = facts.jobs
190
+ .filter((job) => typeof job.uses === "string" && job.uses.trim().length > 0)
191
+ .map((job) => job.id);
192
+ return {
193
+ hasWorkflowCall: workflowCall !== null,
194
+ declaredInputKeys,
195
+ requiredInputKeys,
196
+ declaredSecretKeys,
197
+ requiredSecretKeys,
198
+ jobsWithInheritedSecrets,
199
+ jobsCallingReusableWorkflow,
200
+ };
201
+ }
@@ -7,6 +7,29 @@ function asRecord(value) {
7
7
  function asString(value) {
8
8
  return typeof value === "string" ? value : undefined;
9
9
  }
10
+ function toStringMap(value) {
11
+ const record = asRecord(value);
12
+ if (!record) {
13
+ return undefined;
14
+ }
15
+ const entries = {};
16
+ for (const [key, entry] of Object.entries(record)) {
17
+ if (typeof entry === "string") {
18
+ entries[key] = entry;
19
+ }
20
+ }
21
+ return Object.keys(entries).length > 0 ? entries : undefined;
22
+ }
23
+ function extractNeeds(value) {
24
+ if (typeof value === "string") {
25
+ const normalized = value.trim();
26
+ return normalized.length > 0 ? [normalized] : [];
27
+ }
28
+ if (!Array.isArray(value)) {
29
+ return [];
30
+ }
31
+ return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
32
+ }
10
33
  function normalizeWorkflowPath(value) {
11
34
  return value.replaceAll("\\", "/");
12
35
  }
@@ -31,19 +54,11 @@ function extractStepFacts(step) {
31
54
  if (!stepRecord) {
32
55
  return null;
33
56
  }
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
57
  const stepFacts = {
58
+ if: asString(stepRecord.if),
44
59
  uses: asString(stepRecord.uses),
45
60
  run: asString(stepRecord.run),
46
- with: Object.keys(withEntries).length > 0 ? withEntries : undefined,
61
+ with: toStringMap(stepRecord.with),
47
62
  };
48
63
  if (!stepFacts.uses && !stepFacts.run) {
49
64
  return null;
@@ -61,6 +76,11 @@ function extractJobFacts(id, value) {
61
76
  .filter((step) => step !== null);
62
77
  return {
63
78
  id,
79
+ if: asString(jobRecord.if),
80
+ uses: asString(jobRecord.uses),
81
+ with: toStringMap(jobRecord.with),
82
+ needs: extractNeeds(jobRecord.needs),
83
+ secrets: jobRecord.secrets,
64
84
  permissions: jobRecord.permissions,
65
85
  steps,
66
86
  };
@@ -1,10 +1,16 @@
1
1
  export interface WorkflowStepFacts {
2
+ if?: string;
2
3
  uses?: string;
3
4
  run?: string;
4
5
  with?: Record<string, string>;
5
6
  }
6
7
  export interface WorkflowJobFacts {
7
8
  id: string;
9
+ if?: string;
10
+ uses?: string;
11
+ with?: Record<string, string>;
12
+ needs: string[];
13
+ secrets?: unknown;
8
14
  permissions?: unknown;
9
15
  steps: WorkflowStepFacts[];
10
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codegate-ai",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Pre-flight security scanner for AI coding tool configurations.",
5
5
  "license": "MIT",
6
6
  "type": "module",