Skip to content

Commit bb71076

Browse files
authored
Split unpinned-uses into two separate checks (#205)
1 parent d40c692 commit bb71076

8 files changed

+44
-20
lines changed

Diff for: docs/audits.md

+3
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,9 @@ GitHub Actions will use the latest commit on the referenced repository
544544
This can represent a (small) security risk, as it leaves the calling workflow
545545
at the mercy of the callee action's default branch.
546546

547+
`uses:` clauses with no pin are flagged as *Medium* severity. `uses:` clauses
548+
with a branch or tag pin are flagged as *Low* severity.
549+
547550
### Remediation
548551

549552
For repository actions (like @actions/checkout): add a branch, tag, or SHA

Diff for: src/audit/unpinned_uses.rs

+22-13
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,28 @@ impl WorkflowAudit for UnpinnedUses {
2121
return Ok(vec![]);
2222
};
2323

24-
if uses.unpinned() {
25-
findings.push(
26-
Self::finding()
27-
.confidence(Confidence::High)
28-
.severity(Severity::Informational)
29-
.add_location(
30-
step.location()
31-
.with_keys(&["uses".into()])
32-
.annotated("action is not pinned to a tag, branch, or hash ref"),
33-
)
34-
.build(step.workflow())?,
35-
);
36-
}
24+
let (annotation, severity) = if uses.unpinned() {
25+
(
26+
"action is not pinned to a tag, branch, or hash ref",
27+
Severity::Medium,
28+
)
29+
} else if uses.unhashed() {
30+
("action is not pinned to a hash ref", Severity::Low)
31+
} else {
32+
return Ok(vec![]);
33+
};
34+
35+
findings.push(
36+
Self::finding()
37+
.confidence(Confidence::High)
38+
.severity(severity)
39+
.add_location(
40+
step.location()
41+
.with_keys(&["uses".into()])
42+
.annotated(annotation),
43+
)
44+
.build(step.workflow())?,
45+
);
3746

3847
Ok(findings)
3948
}

Diff for: src/models.rs

+7
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,13 @@ impl<'a> Uses<'a> {
433433
Uses::Repository(repo) => repo.git_ref.is_none(),
434434
}
435435
}
436+
437+
pub(crate) fn unhashed(&self) -> bool {
438+
match self {
439+
Uses::Docker(docker) => docker.hash.is_some(),
440+
Uses::Repository(repo) => !repo.ref_is_commit(),
441+
}
442+
}
436443
}
437444

438445
#[cfg(test)]

Diff for: tests/acceptance.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn audit_artipacked() -> anyhow::Result<()> {
6767
assert_value_match(
6868
&findings,
6969
"$[0].locations[0].concrete.feature",
70-
"uses: actions/checkout@v4",
70+
"uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
7171
);
7272

7373
Ok(())
@@ -188,12 +188,12 @@ fn audit_unpinned_uses() -> anyhow::Result<()> {
188188

189189
let execution = zizmor().args(cli_args).output()?;
190190

191-
assert_eq!(execution.status.code(), Some(11));
191+
assert_eq!(execution.status.code(), Some(13));
192192

193193
let findings = serde_json::from_slice(&execution.stdout)?;
194194

195195
assert_value_match(&findings, "$[0].determinations.confidence", "High");
196-
assert_value_match(&findings, "$[0].determinations.severity", "Informational");
196+
assert_value_match(&findings, "$[0].determinations.severity", "Medium");
197197
assert_value_match(
198198
&findings,
199199
"$[0].locations[0].concrete.feature",

Diff for: tests/test-data/artipacked.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ jobs:
1010
artipacked:
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: actions/checkout@v4
13+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2

Diff for: tests/test-data/inlined-ignores.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
artipacked-ignored:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v4 # zizmor: ignore[artipacked]
11+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # zizmor: ignore[artipacked]
1212

1313
insecure-commands-ignored:
1414
runs-on: ubuntu-latest
@@ -26,3 +26,8 @@ jobs:
2626
password: hackme # zizmor: ignore[hardcoded-container-credentials]
2727
steps:
2828
- run: echo 'This is a honeypot actually!'
29+
30+
unpinned-uses-ignored:
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: github/codeql-action/upload-sarif # zizmor: ignore[unpinned-uses]

Diff for: tests/test-data/template-injection.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111

1212
steps:
13-
- uses: actions/github-script@v7
13+
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1
1414
with:
1515
script: |
1616
return "doing a thing: ${{ github.event.issue.title }}"

Diff for: tests/test-data/use-trusted-publishing.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ jobs:
1010
id-token: write
1111
steps:
1212
- name: vulnerable-2
13-
uses: pypa/gh-action-pypi-publish@release/v1
13+
uses: pypa/gh-action-pypi-publish@release/v1 # zizmor: ignore[unpinned-uses]
1414
with:
1515
password: ${{ secrets.PYPI_TOKEN }}

0 commit comments

Comments
 (0)