codegate-ai 0.9.1 → 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.
- package/dist/layer2-static/detectors/dependabot-auto-merge.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-auto-merge.js +118 -0
- package/dist/layer2-static/detectors/workflow-artifact-trust-chain.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-artifact-trust-chain.js +89 -0
- package/dist/layer2-static/detectors/workflow-call-boundary.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-call-boundary.js +92 -0
- package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-dynamic-matrix-injection.js +149 -0
- package/dist/layer2-static/detectors/workflow-local-action-mutation.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-local-action-mutation.js +125 -0
- package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-oidc-untrusted-context.js +166 -0
- package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-pr-target-checkout-head.js +99 -0
- package/dist/layer2-static/detectors/workflow-secret-exfiltration.d.ts +8 -0
- package/dist/layer2-static/detectors/workflow-secret-exfiltration.js +97 -0
- package/dist/layer2-static/engine.js +89 -0
- package/dist/layer2-static/workflow/analysis.d.ts +24 -0
- package/dist/layer2-static/workflow/analysis.js +201 -0
- package/dist/layer2-static/workflow/parser.js +30 -10
- package/dist/layer2-static/workflow/types.d.ts +6 -0
- 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:
|
|
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
|
}
|