Skip to content
1 change: 1 addition & 0 deletions crates/zizmor/src/audit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub(crate) mod secrets_inherit;
pub(crate) mod self_hosted_runner;
pub(crate) mod stale_action_refs;
pub(crate) mod template_injection;
pub(crate) mod timeout_minutes;
pub(crate) mod undocumented_permissions;
pub(crate) mod unpinned_images;
pub(crate) mod unpinned_uses;
Expand Down
113 changes: 113 additions & 0 deletions crates/zizmor/src/audit/timeout_minutes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use super::{Audit, AuditLoadError, audit_meta};
use crate::{
audit::AuditError,
config::Config,
finding::{
Confidence, Finding, Persona, Severity,
location::Locatable as _,
},
models::workflow::JobExt as _,
state::AuditState,
};

pub(crate) struct TimeoutMinutes;

audit_meta!(
TimeoutMinutes,
"timeout-minutes",
"missing timeout-minutes"
);

#[async_trait::async_trait]
impl Audit for TimeoutMinutes {
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
Ok(Self)
}

async fn audit_normal_job<'doc>(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic here seems pretty deeply nested 🙂 -- maybe we could do this in a "bubble up" style instead, i.e. use audit_step and then check the step's parent (the job) if a timeout is missing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion. I separated out audit_step in f9a4803 but the logic is still quite complex. I wanted to make sure that we don't get reports for both the job and the step when neither set timeout-minutes. Is there a simpler way?

&self,
job: &super::NormalJob<'doc>,
_config: &Config,
) -> anyhow::Result<Vec<Finding<'doc>>, AuditError> {
let mut findings = vec![];

// Check if timeout-minutes is set in the job
match &job.timeout_minutes {
None => 'label: {
// If not, check if it's set in any of the job's steps
for step in job.steps() {
match &step.timeout_minutes {
Some(_) => {
// If so, the job doesn't need to set it
break 'label;
}
None => {}
}
}
findings.push(
Self::finding()
.confidence(Confidence::High)
.severity(Severity::Medium)
.persona(Persona::Pedantic)
.add_location(
job
.location()
.primary()
.annotated("job missing timeout-minutes"),
)
.build(job.parent())?,
);
},
_ => {}
}

Ok(findings)
}

async fn audit_step<'doc>(
&self,
step: &super::Step<'doc>,
_config: &Config,
) -> anyhow::Result<Vec<Finding<'doc>>, AuditError> {
let mut findings = vec![];

// Check if timeout-minutes is set in the step
match &step.timeout_minutes {
None => 'label: {
// If not, check if it's set by its parent job
let job = &step.parent;
match &job.timeout_minutes {
None => {
// If not, check if it's set in any of the job's other steps
for other_step in job.steps() {
match &other_step.timeout_minutes {
Some(_) => {
// If so, this job should set it, too
findings.push(
Self::finding()
.confidence(Confidence::High)
.severity(Severity::Medium)
.persona(Persona::Pedantic)
.add_location(
step
.location()
.primary()
.annotated("step missing timeout-minutes"),
)
.build(step)?,
);
break 'label;
}
None => {}
}
}
},
_ => {}
}
},
_ => {}
}

Ok(findings)
}
}
1 change: 1 addition & 0 deletions crates/zizmor/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ impl AuditRegistry {
register_audit!(audit::dependabot_execution::DependabotExecution);
register_audit!(audit::dependabot_cooldown::DependabotCooldown);
register_audit!(audit::concurrency_limits::ConcurrencyLimits);
register_audit!(audit::timeout_minutes::TimeoutMinutes);

Ok(registry)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/zizmor/tests/integration/e2e/anchors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ fn test_basic() -> Result<()> {
= note: audit confidence → High
= note: this finding has an auto-fix

5 findings (3 suppressed, 2 fixable): 0 informational, 0 low, 0 medium, 2 high
6 findings (4 suppressed, 2 fixable): 0 informational, 0 low, 0 medium, 2 high
"#
);

Expand Down
4 changes: 2 additions & 2 deletions crates/zizmor/tests/integration/e2e/collect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ fn test_warn_deprecated_modes() -> Result<()> {
🌈 zizmor v@@VERSION@@
WARN zizmor: --collect=workflows-only is deprecated; use --collect=workflows instead
WARN zizmor: future versions of zizmor will reject this mode
No findings to report. Good job!
No findings to report. Good job! (1 suppressed)
");

assert_snapshot!(
Expand All @@ -85,7 +85,7 @@ fn test_warn_deprecated_modes() -> Result<()> {
🌈 zizmor v@@VERSION@@
WARN zizmor: --collect=actions-only is deprecated; use --collect=actions instead
WARN zizmor: future versions of zizmor will reject this mode
No findings to report. Good job!
No findings to report. Good job! (1 suppressed)
");

Ok(())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,16 @@ expression: output
"concrete": {
"location": {
"start_point": {
"row": 20,
"row": 21,
"column": 8
},
"end_point": {
"row": 20,
"row": 21,
"column": 29
},
"offset_span": {
"start": 406,
"end": 427
"start": 429,
"end": 450
}
},
"feature": "uses: docker://ubuntu",
Expand Down Expand Up @@ -338,16 +338,16 @@ expression: output
"concrete": {
"location": {
"start_point": {
"row": 26,
"row": 27,
"column": 8
},
"end_point": {
"row": 26,
"row": 27,
"column": 58
},
"offset_span": {
"start": 531,
"end": 581
"start": 554,
"end": 604
}
},
"feature": "uses: docker://ghcr.io/pypa/gh-action-pypi-publish",
Expand Down
4 changes: 2 additions & 2 deletions crates/zizmor/tests/integration/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1100,8 +1100,8 @@ fn concurrency_limits() -> Result<()> {
2 | | on: push
3 | | permissions: {}
... |
10 | | - name: 1-ok
11 | | run: echo ok
11 | | - name: 1-ok
12 | | run: echo ok
| |___________________^ missing concurrency setting
|
= note: audit confidence → High
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 183
expression: "zizmor().input(input_under_test(\"config-scenarios/disablement\")).setenv(\"RUST_LOG\",\n\"zizmor::audit=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG audit{input=Workflow(file://@@INPUT@@/.github/workflows/hackme.yml)}: zizmor::audit: skipping: template-injection is disabled in config for group Group("@@INPUT@@")
No findings to report. Good job! (1 suppressed)
No findings to report. Good job! (2 suppressed)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 116
expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-dotgithub\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
DEBUG zizmor::config: found config candidate at `@@INPUT@@/.github/zizmor.yml`
No findings to report. Good job! (1 ignored, 2 suppressed)
No findings to report. Good job! (1 ignored, 3 suppressed)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 133
expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
DEBUG zizmor::config: attempting config discovery in `@@TEST_PREFIX@@/config-scenarios/config-in-dotgithub/.github/workflows`
DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-dotgithub/.github/zizmor.yml`
No findings to report. Good job! (1 ignored, 2 suppressed)
No findings to report. Good job! (1 ignored, 3 suppressed)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 10
expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-root\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
DEBUG zizmor::config: found config candidate at `@@INPUT@@/zizmor.yml`
No findings to report. Good job! (1 ignored, 2 suppressed)
No findings to report. Good job! (1 ignored, 3 suppressed)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 46
expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-root/.github/workflows\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
DEBUG zizmor::config: attempting config discovery in `@@INPUT@@`
DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-root/zizmor.yml`
No findings to report. Good job! (1 ignored, 2 suppressed)
No findings to report. Good job! (1 ignored, 3 suppressed)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 27
expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-root/.github/workflows/hackme.yml\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
DEBUG zizmor::config: discovering config for local input `@@INPUT@@`
DEBUG zizmor::config: attempting config discovery in `@@TEST_PREFIX@@/config-scenarios/config-in-root/.github/workflows`
DEBUG zizmor::config: found config candidate at `@@TEST_PREFIX@@/config-scenarios/config-in-root/zizmor.yml`
No findings to report. Good job! (1 ignored, 2 suppressed)
No findings to report. Good job! (1 ignored, 3 suppressed)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 150
expression: "zizmor().no_config(true).input(input_under_test(\"config-scenarios/config-in-dotgithub\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -17,4 +18,4 @@ error[template-injection]: code injection via template expansion
|
= note: audit confidence → High

3 findings (2 suppressed): 0 informational, 0 low, 0 medium, 1 high
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 166
expression: "zizmor().no_config(true).input(input_under_test(\"config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -17,4 +18,4 @@ error[template-injection]: code injection via template expansion
|
= note: audit confidence → High

3 findings (2 suppressed): 0 informational, 0 low, 0 medium, 1 high
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 63
expression: "zizmor().no_config(true).input(input_under_test(\"config-scenarios/config-in-root\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -17,4 +18,4 @@ error[template-injection]: code injection via template expansion
|
= note: audit confidence → High

3 findings (2 suppressed): 0 informational, 0 low, 0 medium, 1 high
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 97
expression: "zizmor().no_config(true).input(input_under_test(\"config-scenarios/config-in-root/.github/workflows\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -17,4 +18,4 @@ error[template-injection]: code injection via template expansion
|
= note: audit confidenceHigh

3 findings (2 suppressed): 0 informational, 0 low, 0 medium, 1 high
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/config.rs
assertion_line: 79
expression: "zizmor().no_config(true).input(input_under_test(\"config-scenarios/config-in-root/.github/workflows/hackme.yml\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -17,4 +18,4 @@ error[template-injection]: code injection via template expansion
|
= note: audit confidence → High

3 findings (2 suppressed): 0 informational, 0 low, 0 medium, 1 high
4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/e2e.rs
assertion_line: 307
expression: "zizmor().output(OutputMode::Both).input(input_under_test(\"issue-1065.yml\")).run()?"
---
🌈 zizmor v@@VERSION@@
Expand Down Expand Up @@ -28,4 +29,4 @@ error[unpinned-uses]: unpinned action reference
|
= note: audit confidence → High

5 findings (3 suppressed): 0 informational, 0 low, 1 medium, 1 high
6 findings (4 suppressed): 0 informational, 0 low, 1 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/zizmor/tests/integration/e2e.rs
assertion_line: 73
expression: "zizmor().output(OutputMode::Both).args([\"--collect=all\"]).input(input_under_test(\"e2e-menagerie\")).run()?"
---
🌈 zizmor v@@VERSION@@
Expand All @@ -16,4 +17,4 @@ error[dangerous-triggers]: use of fundamentally insecure workflow trigger
|
= note: audit confidence → Medium

4 findings (3 suppressed): 0 informational, 0 low, 0 medium, 1 high
7 findings (6 suppressed): 0 informational, 0 low, 0 medium, 1 high
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
source: crates/zizmor/tests/integration/e2e.rs
assertion_line: 65
expression: "zizmor().output(OutputMode::Both).input(input_under_test(\"e2e-menagerie\")).run()?"
---
🌈 zizmor v@@VERSION@@
INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/dummy-action-2/action.yml
INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/workflows/another-dummy.yml
INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/workflows/dummy.yml
INFO audit: zizmor: 🌈 completed @@INPUT@@/dummy-action-1/action.yaml
No findings to report. Good job! (2 suppressed)
No findings to report. Good job! (4 suppressed)
Loading