mirror of
https://github.com/zizmorcore/zizmor.git
synced 2025-12-23 08:47:33 +00:00
feat: support composite actions in use-trusted-publishing (#899)
This commit is contained in:
parent
ad62fd42c6
commit
a1252c260c
6 changed files with 127 additions and 53 deletions
|
|
@ -1,15 +1,10 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use github_actions_models::{
|
||||
common::{EnvValue, Uses},
|
||||
workflow::job::StepBody,
|
||||
};
|
||||
use github_actions_models::common::{EnvValue, Uses};
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use super::{Audit, AuditLoadError, audit_meta};
|
||||
use crate::{
|
||||
finding::{Confidence, Severity, location::Locatable as _},
|
||||
models::uses::RepositoryUsesExt as _,
|
||||
finding::{Confidence, Finding, Severity},
|
||||
models::{StepBodyCommon, StepCommon, uses::RepositoryUsesExt as _},
|
||||
state::AuditState,
|
||||
};
|
||||
|
||||
|
|
@ -30,6 +25,58 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl UseTrustedPublishing {
|
||||
fn process_step<'doc>(
|
||||
&self,
|
||||
step: &impl StepCommon<'doc>,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
let StepBodyCommon::Uses {
|
||||
uses: Uses::Repository(uses),
|
||||
with,
|
||||
} = &step.body()
|
||||
else {
|
||||
return Ok(findings);
|
||||
};
|
||||
|
||||
let candidate = Self::finding()
|
||||
.severity(Severity::Informational)
|
||||
.confidence(Confidence::High)
|
||||
.add_location(
|
||||
step.location()
|
||||
.primary()
|
||||
.with_keys(&["uses".into()])
|
||||
.annotated("this step"),
|
||||
);
|
||||
|
||||
if uses.matches("pypa/gh-action-pypi-publish")
|
||||
&& self.pypi_publish_uses_manual_credentials(with)
|
||||
{
|
||||
findings.push(
|
||||
candidate
|
||||
.add_location(
|
||||
step.location()
|
||||
.primary()
|
||||
.with_keys(&["with".into(), "password".into()])
|
||||
.annotated(USES_MANUAL_CREDENTIAL),
|
||||
)
|
||||
.build(step)?,
|
||||
);
|
||||
} else if ((uses.matches("rubygems/release-gem"))
|
||||
&& self.release_gem_uses_manual_credentials(with))
|
||||
|| (uses.matches("rubygems/configure-rubygems-credential")
|
||||
&& self.rubygems_credential_uses_manual_credentials(with))
|
||||
{
|
||||
findings.push(
|
||||
candidate
|
||||
.add_location(step.location().primary().annotated(USES_MANUAL_CREDENTIAL))
|
||||
.build(step)?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
fn pypi_publish_uses_manual_credentials(&self, with: &IndexMap<String, EnvValue>) -> bool {
|
||||
// `password` implies the step isn't using Trusted Publishing,
|
||||
// but we also need to check `repository-url` to prevent false-positives
|
||||
|
|
@ -75,51 +122,13 @@ impl Audit for UseTrustedPublishing {
|
|||
&self,
|
||||
step: &super::Step<'doc>,
|
||||
) -> anyhow::Result<Vec<super::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
self.process_step(step)
|
||||
}
|
||||
|
||||
let StepBody::Uses {
|
||||
uses: Uses::Repository(uses),
|
||||
with,
|
||||
} = &step.deref().body
|
||||
else {
|
||||
return Ok(findings);
|
||||
};
|
||||
|
||||
let candidate = Self::finding()
|
||||
.severity(Severity::Informational)
|
||||
.confidence(Confidence::High)
|
||||
.add_location(
|
||||
step.location()
|
||||
.primary()
|
||||
.with_keys(&["uses".into()])
|
||||
.annotated("this step"),
|
||||
);
|
||||
|
||||
if uses.matches("pypa/gh-action-pypi-publish")
|
||||
&& self.pypi_publish_uses_manual_credentials(with)
|
||||
{
|
||||
findings.push(
|
||||
candidate
|
||||
.add_location(
|
||||
step.location()
|
||||
.primary()
|
||||
.with_keys(&["with".into(), "password".into()])
|
||||
.annotated(USES_MANUAL_CREDENTIAL),
|
||||
)
|
||||
.build(step.workflow())?,
|
||||
);
|
||||
} else if ((uses.matches("rubygems/release-gem"))
|
||||
&& self.release_gem_uses_manual_credentials(with))
|
||||
|| (uses.matches("rubygems/configure-rubygems-credential")
|
||||
&& self.rubygems_credential_uses_manual_credentials(with))
|
||||
{
|
||||
findings.push(
|
||||
candidate
|
||||
.add_location(step.location().primary().annotated(USES_MANUAL_CREDENTIAL))
|
||||
.build(step.workflow())?,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
fn audit_composite_step<'doc>(
|
||||
&self,
|
||||
step: &crate::models::CompositeStep<'doc>,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,25 @@ fn unpinned_uses() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn use_trusted_publishing() -> Result<()> {
|
||||
insta::assert_snapshot!(
|
||||
zizmor()
|
||||
.input(input_under_test("use-trusted-publishing.yml"))
|
||||
.run()?
|
||||
);
|
||||
|
||||
insta::assert_snapshot!(
|
||||
zizmor()
|
||||
.input(input_under_test(
|
||||
"use-trusted-publishing/demo-action/action.yml"
|
||||
))
|
||||
.run()?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insecure_commands() -> Result<()> {
|
||||
insta::assert_snapshot!(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/zizmor/tests/integration/snapshot.rs
|
||||
expression: "zizmor().input(input_under_test(\"use-trusted-publishing/demo-action/action.yml\")).run()?"
|
||||
---
|
||||
info[use-trusted-publishing]: prefer trusted publishing for authentication
|
||||
--> @@INPUT@@:9:7
|
||||
|
|
||||
9 | - uses: pypa/gh-action-pypi-publish@release/v1 # zizmor: ignore[unpinned-uses]
|
||||
| -------------------------------------------- info: this step
|
||||
10 | with:
|
||||
11 | password: ${{ secrets.PYPI_TOKEN }}
|
||||
| ----------------------------------- info: uses a manually-configured credential instead of Trusted Publishing
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
2 findings (1 ignored): 0 unknown, 1 informational, 0 low, 0 medium, 0 high
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
source: crates/zizmor/tests/integration/snapshot.rs
|
||||
expression: "zizmor().input(input_under_test(\"use-trusted-publishing.yml\")).run()?"
|
||||
---
|
||||
info[use-trusted-publishing]: prefer trusted publishing for authentication
|
||||
--> @@INPUT@@:13:9
|
||||
|
|
||||
13 | uses: pypa/gh-action-pypi-publish@release/v1 # zizmor: ignore[unpinned-uses]
|
||||
| -------------------------------------------- info: this step
|
||||
14 | with:
|
||||
15 | password: ${{ secrets.PYPI_TOKEN }}
|
||||
| ----------------------------------- info: uses a manually-configured credential instead of Trusted Publishing
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
3 findings (1 ignored, 1 suppressed): 0 unknown, 1 informational, 0 low, 0 medium, 0 high
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# demo of a composite action being flagged by use-trusted-publishing
|
||||
|
||||
name: use-trusted-publishing-composite-action
|
||||
description: use-trusted-publishing-composite-action
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: pypa/gh-action-pypi-publish@release/v1 # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
password: ${{ secrets.PYPI_TOKEN }}
|
||||
|
|
@ -13,6 +13,8 @@ of `zizmor`.
|
|||
|
||||
* The [artipacked] audit now produces findings on composite action definitions,
|
||||
rather than just workflow definitions (#896)
|
||||
* The [use-trusted-publishing] audit now produces findings on composite
|
||||
action definitions, rather than just workflow definitions (#899)
|
||||
|
||||
### Bug Fixes 🐛
|
||||
|
||||
|
|
@ -841,3 +843,4 @@ This is one of `zizmor`'s bigger recent releases! Key enhancements include:
|
|||
[unsound-contains]: ./audits.md#unsound-contains
|
||||
[unpinned-images]: ./audits.md#unpinned-images
|
||||
[insecure-commands]: ./audits.md#insecure-commands
|
||||
[use-trusted-publishing]: ./audits.md#use-trusted-publishing
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue