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.
- package/README.md +109 -40
- package/dist/cli.js +85 -7
- package/dist/commands/scan-command.d.ts +6 -1
- package/dist/commands/scan-command.js +1 -1
- package/dist/commands/scan-content-command.d.ts +16 -0
- package/dist/commands/scan-content-command.js +65 -0
- package/dist/config/inline-ignore.d.ts +11 -0
- package/dist/config/inline-ignore.js +57 -0
- package/dist/config/suppression-policy.d.ts +22 -0
- package/dist/config/suppression-policy.js +146 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +188 -3
- package/dist/layer2-static/action/parser.d.ts +3 -0
- package/dist/layer2-static/action/parser.js +165 -0
- package/dist/layer2-static/action/types.d.ts +39 -0
- package/dist/layer2-static/action/types.js +1 -0
- package/dist/layer2-static/advisories/agent-components.json +62 -0
- package/dist/layer2-static/advisories/cache.d.ts +6 -0
- package/dist/layer2-static/advisories/cache.js +44 -0
- package/dist/layer2-static/advisories/cache.ts +62 -0
- package/dist/layer2-static/advisories/gha-advisory-client.d.ts +7 -0
- package/dist/layer2-static/advisories/gha-advisory-client.js +4 -0
- package/dist/layer2-static/advisories/gha-advisory-client.ts +13 -0
- package/dist/layer2-static/advisories/gha-known-vulnerable-actions.json +4 -0
- package/dist/layer2-static/audits/registry.d.ts +3 -0
- package/dist/layer2-static/audits/registry.js +29 -0
- package/dist/layer2-static/audits/types.d.ts +12 -0
- package/dist/layer2-static/audits/types.js +1 -0
- package/dist/layer2-static/dependabot/parser.d.ts +3 -0
- package/dist/layer2-static/dependabot/parser.js +189 -0
- package/dist/layer2-static/dependabot/types.d.ts +50 -0
- package/dist/layer2-static/dependabot/types.js +1 -0
- package/dist/layer2-static/detectors/advisory-intelligence.d.ts +7 -0
- package/dist/layer2-static/detectors/advisory-intelligence.js +170 -0
- package/dist/layer2-static/detectors/command-exec.js +6 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-cooldown.js +54 -0
- package/dist/layer2-static/detectors/dependabot-execution.d.ts +7 -0
- package/dist/layer2-static/detectors/dependabot-execution.js +68 -0
- package/dist/layer2-static/detectors/rule-file.js +5 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-anonymous-definition.js +48 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-archived-uses.js +147 -0
- package/dist/layer2-static/detectors/workflow-artipacked.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-artipacked.js +62 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-bot-conditions.js +108 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-cache-poisoning.js +67 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-concurrency-limits.js +76 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-dangerous-triggers.js +38 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-excessive-permissions.js +65 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.d.ts +12 -0
- package/dist/layer2-static/detectors/workflow-forbidden-uses.js +148 -0
- package/dist/layer2-static/detectors/workflow-github-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-github-env.js +49 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-hardcoded-container-credentials.js +90 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-impostor-commit.js +127 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-insecure-commands.js +72 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.d.ts +8 -0
- package/dist/layer2-static/detectors/workflow-known-vuln-action.js +70 -0
- package/dist/layer2-static/detectors/workflow-misfeature.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-misfeature.js +101 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-obfuscation.js +79 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-overprovisioned-secrets.js +81 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-ref-confusion.js +69 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.d.ts +9 -0
- package/dist/layer2-static/detectors/workflow-ref-version-mismatch.js +179 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-inherit.js +59 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-secrets-outside-env.js +99 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-self-hosted-runner.js +67 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-stale-action-refs.js +178 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-superfluous-actions.js +70 -0
- package/dist/layer2-static/detectors/workflow-template-injection.d.ts +6 -0
- package/dist/layer2-static/detectors/workflow-template-injection.js +94 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-undocumented-permissions.js +101 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-images.js +108 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unpinned-uses.js +60 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unredacted-secrets.js +92 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-condition.js +90 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-unsound-contains.js +88 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.d.ts +7 -0
- package/dist/layer2-static/detectors/workflow-use-trusted-publishing.js +66 -0
- package/dist/layer2-static/engine.d.ts +13 -2
- package/dist/layer2-static/engine.js +578 -41
- package/dist/layer2-static/github/cache.d.ts +8 -0
- package/dist/layer2-static/github/cache.js +41 -0
- package/dist/layer2-static/github/client.d.ts +17 -0
- package/dist/layer2-static/github/client.js +43 -0
- package/dist/layer2-static/rule-engine.d.ts +1 -1
- package/dist/layer2-static/rule-engine.js +1 -13
- package/dist/layer2-static/rule-pack-loader.d.ts +10 -0
- package/dist/layer2-static/rule-pack-loader.js +187 -0
- package/dist/layer2-static/workflow/injection-sinks.json +5 -0
- package/dist/layer2-static/workflow/parser.d.ts +3 -0
- package/dist/layer2-static/workflow/parser.js +88 -0
- package/dist/layer2-static/workflow/types.d.ts +15 -0
- package/dist/layer2-static/workflow/types.js +1 -0
- package/dist/layer3-dynamic/toxic-flow.js +6 -0
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +11 -10
- package/dist/report/finding-fingerprint.d.ts +5 -0
- package/dist/report/finding-fingerprint.js +47 -0
- package/dist/reporter/markdown.js +25 -3
- package/dist/reporter/sarif.js +25 -5
- package/dist/reporter/terminal.js +37 -0
- package/dist/scan-target/fetch-plan.d.ts +8 -0
- package/dist/scan-target/fetch-plan.js +30 -0
- package/dist/scan-target/staging.js +60 -5
- package/dist/scan.d.ts +7 -1
- package/dist/scan.js +157 -40
- package/dist/types/finding.d.ts +10 -1
- package/dist/types/finding.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const SECRET_KEY_PATTERN = /(token|password|secret|api[_-]?key|private[_-]?key|access[_-]?key)/iu;
|
|
4
|
+
const REDACTED_VALUE_PATTERN = /^\s*\$\{\{\s*secrets\.[^}]+\}\}\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 collectEnvSecrets(envValue, baseField, candidates) {
|
|
12
|
+
const env = asRecord(envValue);
|
|
13
|
+
if (!env) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
for (const [key, value] of Object.entries(env)) {
|
|
17
|
+
if (!SECRET_KEY_PATTERN.test(key) || typeof value !== "string") {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
if (trimmed.length < 8 || REDACTED_VALUE_PATTERN.test(trimmed)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
candidates.push({
|
|
25
|
+
field: `${baseField}.${key}`,
|
|
26
|
+
key,
|
|
27
|
+
value: trimmed,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function gatherSecretCandidates(parsed) {
|
|
32
|
+
const root = asRecord(parsed);
|
|
33
|
+
const candidates = [];
|
|
34
|
+
collectEnvSecrets(root?.env, "env", candidates);
|
|
35
|
+
const jobs = asRecord(root?.jobs);
|
|
36
|
+
if (!jobs) {
|
|
37
|
+
return candidates;
|
|
38
|
+
}
|
|
39
|
+
for (const [jobId, jobValue] of Object.entries(jobs)) {
|
|
40
|
+
const job = asRecord(jobValue);
|
|
41
|
+
if (!job) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
collectEnvSecrets(job.env, `jobs.${jobId}.env`, candidates);
|
|
45
|
+
const steps = Array.isArray(job.steps) ? job.steps : [];
|
|
46
|
+
steps.forEach((stepValue, stepIndex) => {
|
|
47
|
+
const step = asRecord(stepValue);
|
|
48
|
+
if (!step) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
collectEnvSecrets(step.env, `jobs.${jobId}.steps[${stepIndex}].env`, candidates);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
export function detectWorkflowUnredactedSecrets(input) {
|
|
57
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
61
|
+
if (!facts) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
return gatherSecretCandidates(input.parsed).map((candidate) => {
|
|
65
|
+
const evidence = buildFindingEvidence({
|
|
66
|
+
textContent: input.textContent,
|
|
67
|
+
searchTerms: [candidate.key, candidate.value],
|
|
68
|
+
fallbackValue: `${candidate.key} set to plaintext value`,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
rule_id: "unredacted-secrets",
|
|
72
|
+
finding_id: `UNREDACTED_SECRETS-${input.filePath}-${candidate.field}`,
|
|
73
|
+
severity: "HIGH",
|
|
74
|
+
category: "CI_PERMISSIONS",
|
|
75
|
+
layer: "L2",
|
|
76
|
+
file_path: input.filePath,
|
|
77
|
+
location: { field: candidate.field },
|
|
78
|
+
description: "Workflow exposes a plaintext secret-like value in environment configuration",
|
|
79
|
+
affected_tools: ["github-actions"],
|
|
80
|
+
cve: null,
|
|
81
|
+
owasp: ["ASI02"],
|
|
82
|
+
cwe: "CWE-798",
|
|
83
|
+
confidence: "HIGH",
|
|
84
|
+
fixable: false,
|
|
85
|
+
remediation_actions: [
|
|
86
|
+
"Move secret material to GitHub encrypted secrets and reference it via ${{ secrets.* }}",
|
|
87
|
+
],
|
|
88
|
+
evidence: evidence?.evidence ?? null,
|
|
89
|
+
suppressed: false,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface WorkflowUnsoundConditionInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectWorkflowUnsoundCondition(input: WorkflowUnsoundConditionInput): Finding[];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const SENSITIVE_COMMANDS = ["publish", "deploy", "release", "push"];
|
|
4
|
+
function asRecord(value) {
|
|
5
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function gatherStepConditions(parsed) {
|
|
11
|
+
const root = asRecord(parsed);
|
|
12
|
+
const jobs = asRecord(root?.jobs);
|
|
13
|
+
if (!jobs) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const candidates = [];
|
|
17
|
+
for (const [jobId, jobValue] of Object.entries(jobs)) {
|
|
18
|
+
const job = asRecord(jobValue);
|
|
19
|
+
const steps = Array.isArray(job?.steps) ? job.steps : [];
|
|
20
|
+
steps.forEach((stepValue, stepIndex) => {
|
|
21
|
+
const step = asRecord(stepValue);
|
|
22
|
+
const condition = typeof step?.if === "string" ? step.if : undefined;
|
|
23
|
+
if (!condition) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
candidates.push({
|
|
27
|
+
jobId,
|
|
28
|
+
stepIndex,
|
|
29
|
+
condition,
|
|
30
|
+
run: typeof step?.run === "string" ? step.run : undefined,
|
|
31
|
+
uses: typeof step?.uses === "string" ? step.uses : undefined,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return candidates;
|
|
36
|
+
}
|
|
37
|
+
function isAlwaysCondition(condition) {
|
|
38
|
+
const normalized = condition.toLowerCase();
|
|
39
|
+
return normalized.includes("always()");
|
|
40
|
+
}
|
|
41
|
+
function isSensitiveExecution(run, uses) {
|
|
42
|
+
const runValue = run?.toLowerCase() ?? "";
|
|
43
|
+
if (SENSITIVE_COMMANDS.some((token) => runValue.includes(token))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return typeof uses === "string" && uses.trim().length > 0;
|
|
47
|
+
}
|
|
48
|
+
export function detectWorkflowUnsoundCondition(input) {
|
|
49
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
53
|
+
if (!facts) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
const findings = [];
|
|
57
|
+
for (const candidate of gatherStepConditions(input.parsed)) {
|
|
58
|
+
if (!isAlwaysCondition(candidate.condition) ||
|
|
59
|
+
!isSensitiveExecution(candidate.run, candidate.uses)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const evidence = buildFindingEvidence({
|
|
63
|
+
textContent: input.textContent,
|
|
64
|
+
searchTerms: [candidate.condition, candidate.run ?? "", candidate.uses ?? ""],
|
|
65
|
+
fallbackValue: candidate.condition,
|
|
66
|
+
});
|
|
67
|
+
findings.push({
|
|
68
|
+
rule_id: "workflow-unsound-condition",
|
|
69
|
+
finding_id: `WORKFLOW_UNSOUND_CONDITION-${input.filePath}-${candidate.jobId}-${candidate.stepIndex}`,
|
|
70
|
+
severity: "MEDIUM",
|
|
71
|
+
category: "CI_TRIGGER",
|
|
72
|
+
layer: "L2",
|
|
73
|
+
file_path: input.filePath,
|
|
74
|
+
location: { field: `jobs.${candidate.jobId}.steps[${candidate.stepIndex}].if` },
|
|
75
|
+
description: "Workflow uses always() on a sensitive step, which can bypass expected failure gating",
|
|
76
|
+
affected_tools: ["github-actions"],
|
|
77
|
+
cve: null,
|
|
78
|
+
owasp: ["ASI02"],
|
|
79
|
+
cwe: "CWE-754",
|
|
80
|
+
confidence: "HIGH",
|
|
81
|
+
fixable: false,
|
|
82
|
+
remediation_actions: [
|
|
83
|
+
"Replace always() with explicit success and trust-boundary checks for sensitive steps",
|
|
84
|
+
],
|
|
85
|
+
evidence: evidence?.evidence ?? null,
|
|
86
|
+
suppressed: false,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface WorkflowUnsoundContainsInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectWorkflowUnsoundContains(input: WorkflowUnsoundContainsInput): Finding[];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const PRIVILEGED_COMMANDS = ["publish", "deploy", "release", "gh release"];
|
|
4
|
+
const UNTRUSTED_CONTAINS_PATTERNS = [
|
|
5
|
+
/contains\s*\(\s*github\.event\.pull_request\.(?:title|body)/iu,
|
|
6
|
+
/contains\s*\(\s*github\.event\.comment\.body/iu,
|
|
7
|
+
/contains\s*\(\s*github\.event\.issue\.title/iu,
|
|
8
|
+
];
|
|
9
|
+
function asRecord(value) {
|
|
10
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function gatherStepConditions(parsed) {
|
|
16
|
+
const root = asRecord(parsed);
|
|
17
|
+
const jobs = asRecord(root?.jobs);
|
|
18
|
+
if (!jobs) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const candidates = [];
|
|
22
|
+
for (const [jobId, jobValue] of Object.entries(jobs)) {
|
|
23
|
+
const job = asRecord(jobValue);
|
|
24
|
+
const steps = Array.isArray(job?.steps) ? job.steps : [];
|
|
25
|
+
steps.forEach((stepValue, stepIndex) => {
|
|
26
|
+
const step = asRecord(stepValue);
|
|
27
|
+
if (!step || typeof step.if !== "string") {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
candidates.push({
|
|
31
|
+
jobId,
|
|
32
|
+
stepIndex,
|
|
33
|
+
condition: step.if,
|
|
34
|
+
run: typeof step.run === "string" ? step.run : undefined,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return candidates;
|
|
39
|
+
}
|
|
40
|
+
function isUnsoundContains(condition) {
|
|
41
|
+
return UNTRUSTED_CONTAINS_PATTERNS.some((pattern) => pattern.test(condition));
|
|
42
|
+
}
|
|
43
|
+
function isPrivilegedStep(run) {
|
|
44
|
+
const normalized = run?.toLowerCase() ?? "";
|
|
45
|
+
return PRIVILEGED_COMMANDS.some((command) => normalized.includes(command));
|
|
46
|
+
}
|
|
47
|
+
export function detectWorkflowUnsoundContains(input) {
|
|
48
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
52
|
+
if (!facts) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const findings = [];
|
|
56
|
+
for (const candidate of gatherStepConditions(input.parsed)) {
|
|
57
|
+
if (!isUnsoundContains(candidate.condition) || !isPrivilegedStep(candidate.run)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const evidence = buildFindingEvidence({
|
|
61
|
+
textContent: input.textContent,
|
|
62
|
+
searchTerms: [candidate.condition, candidate.run ?? ""],
|
|
63
|
+
fallbackValue: candidate.condition,
|
|
64
|
+
});
|
|
65
|
+
findings.push({
|
|
66
|
+
rule_id: "workflow-unsound-contains",
|
|
67
|
+
finding_id: `WORKFLOW_UNSOUND_CONTAINS-${input.filePath}-${candidate.jobId}-${candidate.stepIndex}`,
|
|
68
|
+
severity: "HIGH",
|
|
69
|
+
category: "CI_TRIGGER",
|
|
70
|
+
layer: "L2",
|
|
71
|
+
file_path: input.filePath,
|
|
72
|
+
location: { field: `jobs.${candidate.jobId}.steps[${candidate.stepIndex}].if` },
|
|
73
|
+
description: "Workflow gates a privileged step with contains() over untrusted event content",
|
|
74
|
+
affected_tools: ["github-actions"],
|
|
75
|
+
cve: null,
|
|
76
|
+
owasp: ["ASI02"],
|
|
77
|
+
cwe: "CWE-20",
|
|
78
|
+
confidence: "HIGH",
|
|
79
|
+
fixable: false,
|
|
80
|
+
remediation_actions: [
|
|
81
|
+
"Avoid trust decisions based on contains() over untrusted titles, bodies, or comments",
|
|
82
|
+
],
|
|
83
|
+
evidence: evidence?.evidence ?? null,
|
|
84
|
+
suppressed: false,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return findings;
|
|
88
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Finding } from "../../types/finding.js";
|
|
2
|
+
export interface WorkflowUseTrustedPublishingInput {
|
|
3
|
+
filePath: string;
|
|
4
|
+
parsed: unknown;
|
|
5
|
+
textContent: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectWorkflowUseTrustedPublishing(input: WorkflowUseTrustedPublishingInput): Finding[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { buildFindingEvidence } from "../evidence.js";
|
|
2
|
+
import { extractWorkflowFacts, isGitHubWorkflowPath } from "../workflow/parser.js";
|
|
3
|
+
const PUBLISH_COMMANDS = ["npm publish", "pnpm publish", "yarn npm publish", "twine upload"];
|
|
4
|
+
const TOKEN_NAMES = ["NODE_AUTH_TOKEN", "NPM_TOKEN", "TWINE_USERNAME", "TWINE_PASSWORD"];
|
|
5
|
+
function containsPublishCommand(value) {
|
|
6
|
+
if (!value) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return PUBLISH_COMMANDS.some((command) => value.includes(command));
|
|
10
|
+
}
|
|
11
|
+
function findTokenName(textContent) {
|
|
12
|
+
for (const tokenName of TOKEN_NAMES) {
|
|
13
|
+
if (textContent.includes(tokenName)) {
|
|
14
|
+
return tokenName;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
export function detectWorkflowUseTrustedPublishing(input) {
|
|
20
|
+
if (!isGitHubWorkflowPath(input.filePath)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const facts = extractWorkflowFacts(input.parsed);
|
|
24
|
+
if (!facts) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const tokenName = findTokenName(input.textContent);
|
|
28
|
+
if (!tokenName) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const findings = [];
|
|
32
|
+
facts.jobs.forEach((job, jobIndex) => {
|
|
33
|
+
job.steps.forEach((step, stepIndex) => {
|
|
34
|
+
if (!containsPublishCommand(step.run)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const evidence = buildFindingEvidence({
|
|
38
|
+
textContent: input.textContent,
|
|
39
|
+
searchTerms: [step.run ?? "", tokenName],
|
|
40
|
+
fallbackValue: step.run ?? tokenName,
|
|
41
|
+
});
|
|
42
|
+
findings.push({
|
|
43
|
+
rule_id: "workflow-use-trusted-publishing",
|
|
44
|
+
finding_id: `WORKFLOW_USE_TRUSTED_PUBLISHING-${input.filePath}-${jobIndex}-${stepIndex}`,
|
|
45
|
+
severity: "HIGH",
|
|
46
|
+
category: "CI_PERMISSIONS",
|
|
47
|
+
layer: "L2",
|
|
48
|
+
file_path: input.filePath,
|
|
49
|
+
location: { field: `jobs.${job.id}.steps[${stepIndex}].run` },
|
|
50
|
+
description: "Package publication uses long-lived registry credentials instead of trusted publishing",
|
|
51
|
+
affected_tools: ["github-actions"],
|
|
52
|
+
cve: null,
|
|
53
|
+
owasp: ["ASI02"],
|
|
54
|
+
cwe: "CWE-798",
|
|
55
|
+
confidence: "HIGH",
|
|
56
|
+
fixable: false,
|
|
57
|
+
remediation_actions: [
|
|
58
|
+
"Switch package publishing to trusted publishing with OIDC and remove registry secrets",
|
|
59
|
+
],
|
|
60
|
+
evidence: evidence?.evidence ?? null,
|
|
61
|
+
suppressed: false,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return findings;
|
|
66
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type GitHookEntry } from "./detectors/git-hooks.js";
|
|
2
2
|
import { type SymlinkEscapeEntry } from "./detectors/symlink.js";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AuditPersona, RuntimeMode } from "../config.js";
|
|
4
|
+
import { type Finding } from "../types/finding.js";
|
|
4
5
|
import type { DiscoveryFormat } from "../types/discovery.js";
|
|
5
6
|
export interface StaticFileInput {
|
|
6
7
|
filePath: string;
|
|
@@ -17,6 +18,16 @@ export interface StaticEngineConfig {
|
|
|
17
18
|
trustedApiDomains: string[];
|
|
18
19
|
unicodeAnalysis: boolean;
|
|
19
20
|
checkIdeSettings: boolean;
|
|
21
|
+
rulePackPaths?: string[];
|
|
22
|
+
allowedRules?: string[];
|
|
23
|
+
skipRules?: string[];
|
|
24
|
+
persona?: AuditPersona;
|
|
25
|
+
runtimeMode?: RuntimeMode;
|
|
26
|
+
workflowAuditsEnabled?: boolean;
|
|
27
|
+
rulePolicies?: Record<string, {
|
|
28
|
+
disable?: boolean;
|
|
29
|
+
config?: Record<string, unknown>;
|
|
30
|
+
}>;
|
|
20
31
|
}
|
|
21
32
|
export interface StaticEngineInput {
|
|
22
33
|
projectRoot: string;
|
|
@@ -25,4 +36,4 @@ export interface StaticEngineInput {
|
|
|
25
36
|
hooks: GitHookEntry[];
|
|
26
37
|
config: StaticEngineConfig;
|
|
27
38
|
}
|
|
28
|
-
export declare function runStaticEngine(input: StaticEngineInput): Finding[]
|
|
39
|
+
export declare function runStaticEngine(input: StaticEngineInput): Promise<Finding[]>;
|