codegate-ai 0.10.0 → 0.12.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/README.md +39 -6
- package/dist/cli.js +24 -0
- package/dist/commands/clawhub-wrapper.d.ts +8 -1
- package/dist/commands/clawhub-wrapper.js +106 -4
- package/dist/commands/skills-wrapper.d.ts +8 -1
- package/dist/commands/skills-wrapper.js +106 -4
- package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +3 -1
- package/dist/layer2-static/detectors/workflow-floating-action-version.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-floating-action-version.js +63 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.js +33 -0
- package/dist/layer2-static/detectors/workflow-github-env.js +89 -28
- package/dist/layer2-static/detectors/workflow-known-vuln-action.js +99 -1
- package/dist/layer2-static/detectors/workflow-ref-confusion.js +30 -0
- package/dist/layer2-static/detectors/workflow-run-untrusted-artifact.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-run-untrusted-artifact.js +126 -0
- package/dist/layer2-static/detectors/workflow-template-injection.js +102 -13
- package/dist/layer2-static/detectors/workflow-unpinned-uses.js +34 -0
- package/dist/layer2-static/detectors/workflow-unsafe-checkout-ref.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsafe-checkout-ref.js +102 -0
- package/dist/layer2-static/engine.js +33 -0
- package/dist/layer2-static/github/client.js +13 -1
- package/package.json +1 -1
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const UNTRUSTED_REF_PATTERNS = [
|
|
4
|
+
/\bgithub\.event\.pull_request\.head\.ref\b/iu,
|
|
5
|
+
/\bgithub\.head_ref\b/iu,
|
|
6
|
+
/\bgithub\.event\.workflow_run\.head_branch\b/iu,
|
|
7
|
+
];
|
|
8
|
+
function hasWritePermission(value) {
|
|
9
|
+
if (typeof value === "string") {
|
|
10
|
+
return value.trim().toLowerCase() === "write-all";
|
|
11
|
+
}
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return Object.values(value).some((permission) => typeof permission === "string" && permission.trim().toLowerCase() === "write");
|
|
16
|
+
}
|
|
17
|
+
function hasIdTokenWrite(value) {
|
|
18
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const idTokenPermission = value["id-token"];
|
|
22
|
+
return (typeof idTokenPermission === "string" && idTokenPermission.trim().toLowerCase() === "write");
|
|
23
|
+
}
|
|
24
|
+
function hasInheritedSecrets(secrets) {
|
|
25
|
+
return typeof secrets === "string" && secrets.trim().toLowerCase() === "inherit";
|
|
26
|
+
}
|
|
27
|
+
function isCheckoutStep(stepUses) {
|
|
28
|
+
if (typeof stepUses !== "string") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return /^actions\/checkout@/iu.test(stepUses.trim());
|
|
32
|
+
}
|
|
33
|
+
function hasUnsafeRef(ref) {
|
|
34
|
+
if (typeof ref !== "string") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return UNTRUSTED_REF_PATTERNS.some((pattern) => pattern.test(ref));
|
|
38
|
+
}
|
|
39
|
+
export function detectWorkflowUnsafeCheckoutRef(input) {
|
|
40
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
44
|
+
if (!facts) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const hasRelevantTrigger = facts.triggers.some((trigger) => {
|
|
48
|
+
const normalized = trigger.trim().toLowerCase();
|
|
49
|
+
return normalized === "pull_request_target" || normalized === "workflow_run";
|
|
50
|
+
});
|
|
51
|
+
if (!hasRelevantTrigger) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const workflowPrivileged = hasWritePermission(facts.workflowPermissions) || hasIdTokenWrite(facts.workflowPermissions);
|
|
55
|
+
const findings = [];
|
|
56
|
+
facts.jobs.forEach((job, jobIndex) => {
|
|
57
|
+
const jobPrivileged = workflowPrivileged ||
|
|
58
|
+
hasWritePermission(job.permissions) ||
|
|
59
|
+
hasIdTokenWrite(job.permissions) ||
|
|
60
|
+
hasInheritedSecrets(job.secrets);
|
|
61
|
+
if (!jobPrivileged) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
job.steps.forEach((step, stepIndex) => {
|
|
65
|
+
if (!isCheckoutStep(step.uses)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const refValue = step.with?.ref;
|
|
69
|
+
if (!hasUnsafeRef(refValue)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const evidence = buildFindingEvidence({
|
|
73
|
+
textContent: input.textContent,
|
|
74
|
+
searchTerms: [step.uses ?? "", refValue ?? "", "head.ref", "head_branch"],
|
|
75
|
+
fallbackValue: `jobs.${job.id}.steps[${stepIndex}].with.ref`,
|
|
76
|
+
});
|
|
77
|
+
findings.push({
|
|
78
|
+
rule_id: "workflow-unsafe-checkout-ref",
|
|
79
|
+
finding_id: `WORKFLOW_UNSAFE_CHECKOUT_REF-${input.filePath}-${jobIndex}-${stepIndex}`,
|
|
80
|
+
severity: "HIGH",
|
|
81
|
+
category: "CI_TEMPLATE_INJECTION",
|
|
82
|
+
layer: "L2",
|
|
83
|
+
file_path: input.filePath,
|
|
84
|
+
location: { field: `jobs.${job.id}.steps[${stepIndex}].with.ref` },
|
|
85
|
+
description: "Privileged checkout references attacker-influenced ref names; prefer immutable commit SHA values",
|
|
86
|
+
affected_tools: ["github-actions"],
|
|
87
|
+
cve: null,
|
|
88
|
+
owasp: ["ASI02"],
|
|
89
|
+
cwe: "CWE-20",
|
|
90
|
+
confidence: "HIGH",
|
|
91
|
+
fixable: false,
|
|
92
|
+
remediation_actions: [
|
|
93
|
+
"Use github.event.pull_request.head.sha (or an immutable SHA) instead of head.ref/head_branch values",
|
|
94
|
+
"Avoid checking out attacker-controlled refs in privileged workflows",
|
|
95
|
+
],
|
|
96
|
+
evidence: evidence?.evidence ?? refValue ?? null,
|
|
97
|
+
suppressed: false,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
return findings;
|
|
102
|
+
}
|
|
@@ -15,6 +15,7 @@ import { detectWorkflowUnpinnedUses } from "./detectors/workflow-unpinned-uses.j
|
|
|
15
15
|
import { detectWorkflowArtipacked } from "./detectors/workflow-artipacked.js";
|
|
16
16
|
import { detectWorkflowCachePoisoning } from "./detectors/workflow-cache-poisoning.js";
|
|
17
17
|
import { detectWorkflowGithubEnv } from "./detectors/workflow-github-env.js";
|
|
18
|
+
import { detectWorkflowRunUntrustedArtifact } from "./detectors/workflow-run-untrusted-artifact.js";
|
|
18
19
|
import { detectWorkflowInsecureCommands } from "./detectors/workflow-insecure-commands.js";
|
|
19
20
|
import { detectWorkflowSelfHostedRunner } from "./detectors/workflow-self-hosted-runner.js";
|
|
20
21
|
import { detectWorkflowOverprovisionedSecrets } from "./detectors/workflow-overprovisioned-secrets.js";
|
|
@@ -28,7 +29,9 @@ import { detectWorkflowForbiddenUses } from "./detectors/workflow-forbidden-uses
|
|
|
28
29
|
import { detectWorkflowRefConfusion } from "./detectors/workflow-ref-confusion.js";
|
|
29
30
|
import { detectWorkflowRefVersionMismatch } from "./detectors/workflow-ref-version-mismatch.js";
|
|
30
31
|
import { detectWorkflowImpostorCommit } from "./detectors/workflow-impostor-commit.js";
|
|
32
|
+
import { detectWorkflowUnsafeCheckoutRef } from "./detectors/workflow-unsafe-checkout-ref.js";
|
|
31
33
|
import { detectWorkflowUnpinnedImages } from "./detectors/workflow-unpinned-images.js";
|
|
34
|
+
import { detectWorkflowFloatingActionVersion } from "./detectors/workflow-floating-action-version.js";
|
|
32
35
|
import { detectWorkflowAnonymousDefinition } from "./detectors/workflow-anonymous-definition.js";
|
|
33
36
|
import { detectWorkflowConcurrencyLimits } from "./detectors/workflow-concurrency-limits.js";
|
|
34
37
|
import { detectWorkflowSuperfluousActions } from "./detectors/workflow-superfluous-actions.js";
|
|
@@ -307,6 +310,16 @@ function buildFileAudits() {
|
|
|
307
310
|
})
|
|
308
311
|
: [],
|
|
309
312
|
},
|
|
313
|
+
{
|
|
314
|
+
id: "workflow-run-untrusted-artifact",
|
|
315
|
+
run: ({ file, input }) => input.config.workflowAuditsEnabled
|
|
316
|
+
? detectWorkflowRunUntrustedArtifact({
|
|
317
|
+
filePath: file.filePath,
|
|
318
|
+
parsed: file.parsed,
|
|
319
|
+
textContent: file.textContent,
|
|
320
|
+
})
|
|
321
|
+
: [],
|
|
322
|
+
},
|
|
310
323
|
{
|
|
311
324
|
id: "workflow-insecure-commands",
|
|
312
325
|
run: ({ file, input }) => input.config.workflowAuditsEnabled
|
|
@@ -432,6 +445,26 @@ function buildFileAudits() {
|
|
|
432
445
|
})
|
|
433
446
|
: [],
|
|
434
447
|
},
|
|
448
|
+
{
|
|
449
|
+
id: "workflow-unsafe-checkout-ref",
|
|
450
|
+
run: ({ file, input }) => input.config.workflowAuditsEnabled
|
|
451
|
+
? detectWorkflowUnsafeCheckoutRef({
|
|
452
|
+
filePath: file.filePath,
|
|
453
|
+
parsed: file.parsed,
|
|
454
|
+
textContent: file.textContent,
|
|
455
|
+
})
|
|
456
|
+
: [],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
id: "workflow-floating-action-version",
|
|
460
|
+
run: ({ file, input }) => input.config.workflowAuditsEnabled
|
|
461
|
+
? detectWorkflowFloatingActionVersion({
|
|
462
|
+
filePath: file.filePath,
|
|
463
|
+
parsed: file.parsed,
|
|
464
|
+
textContent: file.textContent,
|
|
465
|
+
})
|
|
466
|
+
: [],
|
|
467
|
+
},
|
|
435
468
|
{
|
|
436
469
|
id: "workflow-impostor-commit",
|
|
437
470
|
onlineRequired: true,
|
|
@@ -9,6 +9,15 @@ function normalizeAdvisoryMap(value) {
|
|
|
9
9
|
}
|
|
10
10
|
return normalized;
|
|
11
11
|
}
|
|
12
|
+
function mergeAdvisoryMaps(bundled, cached) {
|
|
13
|
+
const merged = {};
|
|
14
|
+
const allKeys = new Set([...Object.keys(bundled), ...Object.keys(cached)]);
|
|
15
|
+
for (const key of allKeys) {
|
|
16
|
+
const versions = new Set([...(bundled[key] ?? []), ...(cached[key] ?? [])]);
|
|
17
|
+
merged[key] = Array.from(versions);
|
|
18
|
+
}
|
|
19
|
+
return merged;
|
|
20
|
+
}
|
|
12
21
|
export function createGithubMetadataClient(options = {}) {
|
|
13
22
|
const runtimeMode = options.runtimeMode ?? "offline";
|
|
14
23
|
const cacheDir = options.cacheDir ?? join(homedir(), ".codegate", "cache");
|
|
@@ -31,7 +40,10 @@ export function createGithubMetadataClient(options = {}) {
|
|
|
31
40
|
}
|
|
32
41
|
const cached = loadCachedAdvisoryPayload(cacheDir, cacheMaxAgeMs, now);
|
|
33
42
|
if (cached) {
|
|
34
|
-
return
|
|
43
|
+
return {
|
|
44
|
+
generatedAt: Math.max(payload.generatedAt, cached.generatedAt),
|
|
45
|
+
advisories: mergeAdvisoryMaps(payload.advisories, cached.advisories),
|
|
46
|
+
};
|
|
35
47
|
}
|
|
36
48
|
saveCachedAdvisoryPayload(cacheDir, payload);
|
|
37
49
|
return payload;
|