From d75933e72da16b3899fefede82ac3dfd8abaf041 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 27 Aug 2025 23:39:13 -0400 Subject: [PATCH] feat: load separate configs for input groups (#1094) --- .../zizmor/src/audit/anonymous_definition.rs | 3 +- crates/zizmor/src/audit/artipacked.rs | 20 +- crates/zizmor/src/audit/bot_conditions.rs | 14 +- crates/zizmor/src/audit/cache_poisoning.rs | 21 +- crates/zizmor/src/audit/dangerous_triggers.rs | 9 +- .../zizmor/src/audit/excessive_permissions.rs | 3 +- crates/zizmor/src/audit/forbidden_uses.rs | 51 +- crates/zizmor/src/audit/github_env.rs | 25 +- .../audit/hardcoded_container_credentials.rs | 3 +- crates/zizmor/src/audit/impostor_commit.rs | 10 +- crates/zizmor/src/audit/insecure_commands.rs | 21 +- .../src/audit/known_vulnerable_actions.rs | 76 ++- crates/zizmor/src/audit/mod.rs | 62 ++- crates/zizmor/src/audit/obfuscation.rs | 16 +- .../src/audit/overprovisioned_secrets.rs | 1 + crates/zizmor/src/audit/ref_confusion.rs | 9 +- crates/zizmor/src/audit/secrets_inherit.rs | 3 +- crates/zizmor/src/audit/self_hosted_runner.rs | 3 +- crates/zizmor/src/audit/stale_action_refs.rs | 11 +- crates/zizmor/src/audit/template_injection.rs | 21 +- crates/zizmor/src/audit/unpinned_images.rs | 3 +- crates/zizmor/src/audit/unpinned_uses.rs | 200 +------- crates/zizmor/src/audit/unredacted_secrets.rs | 3 +- crates/zizmor/src/audit/unsound_condition.rs | 5 +- crates/zizmor/src/audit/unsound_contains.rs | 3 +- .../src/audit/use_trusted_publishing.rs | 4 +- crates/zizmor/src/config.rs | 442 ++++++++++++++++-- crates/zizmor/src/finding.rs | 10 +- crates/zizmor/src/github_api.rs | 117 ++++- crates/zizmor/src/lsp.rs | 27 +- crates/zizmor/src/main.rs | 82 +++- crates/zizmor/src/models/uses.rs | 2 +- crates/zizmor/src/registry.rs | 14 +- crates/zizmor/src/registry/input.rs | 135 +++--- crates/zizmor/src/state.rs | 57 +-- crates/zizmor/tests/integration/common.rs | 55 ++- crates/zizmor/tests/integration/config.rs | 178 +++++++ crates/zizmor/tests/integration/main.rs | 2 + ...config__discovers_config_in_dotgithub.snap | 9 + ...s_config_in_dotgithub_from_file_input.snap | 9 + ...ion__config__discovers_config_in_root.snap | 9 + ...scovers_config_in_root_from_child_dir.snap | 9 + ...covers_config_in_root_from_file_input.snap | 9 + ...__config__ignores_config_in_dotgithub.snap | 20 + ...s_config_in_dotgithub_from_file_input.snap | 20 + ...ation__config__ignores_config_in_root.snap | 20 + ...ignores_config_in_root_from_child_dir.snap | 20 + ...gnores_config_in_root_from_file_input.snap | 20 + .../integration__e2e__gha_hazmat.snap | 1 - ...integration__e2e__invalid_config_file.snap | 11 +- .../integration__e2e__invalid_inputs-10.snap | 1 + .../integration__e2e__invalid_inputs-2.snap | 1 + .../integration__e2e__invalid_inputs-3.snap | 1 + .../integration__e2e__invalid_inputs-4.snap | 1 + .../integration__e2e__invalid_inputs-5.snap | 1 + .../integration__e2e__invalid_inputs-6.snap | 1 + .../integration__e2e__invalid_inputs-7.snap | 1 + .../integration__e2e__invalid_inputs-8.snap | 1 + .../integration__e2e__invalid_inputs-9.snap | 1 + .../integration__e2e__invalid_inputs.snap | 1 + .../integration__e2e__issue_569.snap | 16 +- .../integration__e2e__issue_726.snap | 1 - .../integration__e2e__menagerie-2.snap | 2 +- .../integration__e2e__menagerie.snap | 2 +- .../integration__e2e__pr_960_backstop.snap | 2 +- .../integration__snapshot__artipacked-2.snap | 1 + .../integration__snapshot__artipacked-3.snap | 1 + .../integration__snapshot__artipacked-4.snap | 1 + .../integration__snapshot__artipacked-5.snap | 1 + .../integration__snapshot__artipacked.snap | 1 + ...integration__snapshot__bot_conditions.snap | 1 + ...gration__snapshot__cache_poisoning-10.snap | 1 + ...gration__snapshot__cache_poisoning-11.snap | 1 + ...gration__snapshot__cache_poisoning-15.snap | 1 + ...egration__snapshot__cache_poisoning-3.snap | 1 + ...egration__snapshot__cache_poisoning-5.snap | 1 + ...egration__snapshot__cache_poisoning-9.snap | 1 + ...n__snapshot__excessive_permissions-10.snap | 1 + ...n__snapshot__excessive_permissions-11.snap | 1 + ...n__snapshot__excessive_permissions-12.snap | 1 + ...on__snapshot__excessive_permissions-2.snap | 1 + ...on__snapshot__excessive_permissions-4.snap | 1 + ...on__snapshot__excessive_permissions-5.snap | 1 + ...on__snapshot__excessive_permissions-7.snap | 1 + ...on__snapshot__excessive_permissions-8.snap | 1 + ...tegration__snapshot__forbidden_uses-2.snap | 1 + ...tegration__snapshot__forbidden_uses-3.snap | 1 + ...tegration__snapshot__forbidden_uses-4.snap | 1 + ...tegration__snapshot__forbidden_uses-5.snap | 1 + ...tegration__snapshot__forbidden_uses-6.snap | 1 + .../integration__snapshot__github_env-2.snap | 1 + .../integration__snapshot__github_env-3.snap | 1 + .../integration__snapshot__github_env.snap | 3 +- .../integration__snapshot__github_output.snap | 1 + ...ration__snapshot__insecure_commands-2.snap | 1 + ...ration__snapshot__insecure_commands-3.snap | 1 + ...ration__snapshot__insecure_commands-4.snap | 1 + ...egration__snapshot__insecure_commands.snap | 1 + .../integration__snapshot__obfuscation-2.snap | 1 + .../integration__snapshot__obfuscation.snap | 1 + ...on__snapshot__overprovisioned_secrets.snap | 1 + ...ntegration__snapshot__secrets_inherit.snap | 1 + .../integration__snapshot__self_hosted-3.snap | 1 + .../integration__snapshot__self_hosted-4.snap | 1 + .../integration__snapshot__self_hosted-5.snap | 1 + .../integration__snapshot__self_hosted-6.snap | 1 + .../integration__snapshot__self_hosted.snap | 1 + ...tion__snapshot__template_injection-11.snap | 1 + ...tion__snapshot__template_injection-12.snap | 1 + ...tion__snapshot__template_injection-13.snap | 1 + ...tion__snapshot__template_injection-14.snap | 1 + ...ation__snapshot__template_injection-2.snap | 1 + ...ation__snapshot__template_injection-4.snap | 1 + ...ation__snapshot__template_injection-5.snap | 1 + ...ation__snapshot__template_injection-6.snap | 1 + ...gration__snapshot__template_injection.snap | 1 + ...ntegration__snapshot__unpinned_images.snap | 1 + ...tegration__snapshot__unpinned_uses-10.snap | 12 +- ...tegration__snapshot__unpinned_uses-11.snap | 12 +- ...tegration__snapshot__unpinned_uses-12.snap | 12 +- ...ntegration__snapshot__unpinned_uses-3.snap | 3 +- ...ntegration__snapshot__unpinned_uses-6.snap | 12 +- ...ntegration__snapshot__unpinned_uses-7.snap | 12 +- ...ntegration__snapshot__unpinned_uses-8.snap | 12 +- ...ntegration__snapshot__unpinned_uses-9.snap | 12 +- ...gration__snapshot__unredacted_secrets.snap | 1 + ...tegration__snapshot__unsound_contains.snap | 1 + ...n__snapshot__use_trusted_publishing-2.snap | 1 + .../.github/workflows/hackme.yml | 16 + .../config-in-dotgithub/.github/zizmor.yml | 4 + .../.github/workflows/hackme.yml | 16 + .../config-in-root/zizmor.yml | 4 + docs/configuration.md | 53 ++- docs/release-notes.md | 32 ++ docs/snippets/help.txt | 2 +- docs/usage.md | 10 +- 136 files changed, 1528 insertions(+), 664 deletions(-) create mode 100644 crates/zizmor/tests/integration/config.rs create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub_from_file_input.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_child_dir.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_file_input.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub_from_file_input.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_child_dir.snap create mode 100644 crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_file_input.snap create mode 100644 crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml create mode 100644 crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/zizmor.yml create mode 100644 crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/.github/workflows/hackme.yml create mode 100644 crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/zizmor.yml diff --git a/crates/zizmor/src/audit/anonymous_definition.rs b/crates/zizmor/src/audit/anonymous_definition.rs index 786525c3..3c03477a 100644 --- a/crates/zizmor/src/audit/anonymous_definition.rs +++ b/crates/zizmor/src/audit/anonymous_definition.rs @@ -19,13 +19,14 @@ audit_meta!( ); impl Audit for AnonymousDefinition { - fn new(_state: &AuditState<'_>) -> Result { + fn new(_state: &AuditState) -> Result { Ok(Self) } fn audit_workflow<'doc>( &self, workflow: &'doc crate::models::workflow::Workflow, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/artipacked.rs b/crates/zizmor/src/audit/artipacked.rs index 32775d93..fefe7b91 100644 --- a/crates/zizmor/src/audit/artipacked.rs +++ b/crates/zizmor/src/audit/artipacked.rs @@ -162,13 +162,14 @@ impl Artipacked { } impl Audit for Artipacked { - fn new(_state: &AuditState<'_>) -> Result { + fn new(_state: &AuditState) -> Result { Ok(Self) } fn audit_action<'doc>( &self, action: &'doc crate::models::action::Action, + _config: &crate::config::Config, ) -> anyhow::Result>> { let Some(steps) = action.steps() else { return Ok(vec![]); @@ -177,7 +178,11 @@ impl Audit for Artipacked { self.process_steps(steps) } - fn audit_normal_job<'doc>(&self, job: &super::NormalJob<'doc>) -> Result>> { + fn audit_normal_job<'doc>( + &self, + job: &super::NormalJob<'doc>, + _config: &crate::config::Config, + ) -> Result>> { self.process_steps(job.steps()) } } @@ -186,7 +191,7 @@ impl Audit for Artipacked { mod tests { use super::*; use crate::{ - github_api::GitHubHost, + config::Config, models::{AsDocument, workflow::Workflow}, registry::input::InputKey, state::AuditState, @@ -205,14 +210,9 @@ mod tests { ($audit_type:ty, $filename:expr, $workflow_content:expr, $test_fn:expr) => {{ let key = InputKey::local("fakegroup".into(), $filename, None::<&str>).unwrap(); let workflow = Workflow::from_string($workflow_content.to_string(), key).unwrap(); - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let audit = <$audit_type>::new(&audit_state).unwrap(); - let findings = audit.audit_workflow(&workflow).unwrap(); + let findings = audit.audit_workflow(&workflow, &Config::default()).unwrap(); $test_fn(&workflow, findings) }}; diff --git a/crates/zizmor/src/audit/bot_conditions.rs b/crates/zizmor/src/audit/bot_conditions.rs index a06b840d..d2da58a4 100644 --- a/crates/zizmor/src/audit/bot_conditions.rs +++ b/crates/zizmor/src/audit/bot_conditions.rs @@ -54,7 +54,7 @@ const BOT_ACTOR_IDS: &[&str] = &[ ]; impl Audit for BotConditions { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -64,6 +64,7 @@ impl Audit for BotConditions { fn audit_normal_job<'doc>( &self, job: &super::NormalJob<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; @@ -404,8 +405,8 @@ impl BotConditions { mod tests { use super::*; use crate::{ + config::Config, finding::Finding, - github_api::GitHubHost, models::{AsDocument, workflow::Workflow}, registry::input::InputKey, state::AuditState, @@ -416,14 +417,9 @@ mod tests { ($audit_type:ty, $filename:expr, $workflow_content:expr, $test_fn:expr) => {{ let key = InputKey::local("fakegroup".into(), $filename, None::<&str>).unwrap(); let workflow = Workflow::from_string($workflow_content.to_string(), key).unwrap(); - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let audit = <$audit_type>::new(&audit_state).unwrap(); - let findings = audit.audit_workflow(&workflow).unwrap(); + let findings = audit.audit_workflow(&workflow, &Config::default()).unwrap(); $test_fn(&workflow, findings) }}; diff --git a/crates/zizmor/src/audit/cache_poisoning.rs b/crates/zizmor/src/audit/cache_poisoning.rs index 46cc27d2..8b6de714 100644 --- a/crates/zizmor/src/audit/cache_poisoning.rs +++ b/crates/zizmor/src/audit/cache_poisoning.rs @@ -4,6 +4,7 @@ use github_actions_models::workflow::Trigger; use github_actions_models::workflow::event::{BareEvent, BranchFilters, OptionalBody}; use crate::audit::{Audit, audit_meta}; +use crate::config::Config; use crate::finding::location::{Locatable as _, Routable}; use crate::finding::{Confidence, Finding, Fix, FixDisposition, Severity}; use crate::models::StepCommon; @@ -442,14 +443,18 @@ impl CachePoisoning { } impl Audit for CachePoisoning { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { Ok(Self) } - fn audit_normal_job<'doc>(&self, job: &NormalJob<'doc>) -> anyhow::Result>> { + fn audit_normal_job<'doc>( + &self, + job: &NormalJob<'doc>, + _config: &Config, + ) -> anyhow::Result>> { let mut findings = vec![]; let steps = job.steps(); let trigger = &job.parent().on; @@ -472,8 +477,7 @@ impl Audit for CachePoisoning { mod tests { use super::*; use crate::{ - github_api::GitHubHost, models::workflow::Workflow, registry::input::InputKey, - state::AuditState, + config::Config, models::workflow::Workflow, registry::input::InputKey, state::AuditState, }; /// Macro for testing workflow audits with common boilerplate @@ -489,14 +493,9 @@ mod tests { ($audit_type:ty, $filename:expr, $workflow_content:expr, $test_fn:expr) => {{ let key = InputKey::local("fakegroup".into(), $filename, None::<&str>).unwrap(); let workflow = Workflow::from_string($workflow_content.to_string(), key).unwrap(); - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let audit = <$audit_type>::new(&audit_state).unwrap(); - let findings = audit.audit_workflow(&workflow).unwrap(); + let findings = audit.audit_workflow(&workflow, &Config::default()).unwrap(); $test_fn(findings) }}; diff --git a/crates/zizmor/src/audit/dangerous_triggers.rs b/crates/zizmor/src/audit/dangerous_triggers.rs index e7c71f4c..c0f0cf4a 100644 --- a/crates/zizmor/src/audit/dangerous_triggers.rs +++ b/crates/zizmor/src/audit/dangerous_triggers.rs @@ -1,6 +1,7 @@ use anyhow::Result; use super::{Audit, AuditLoadError, audit_meta}; +use crate::config::Config; use crate::finding::{Confidence, Finding, Severity}; use crate::models::workflow::Workflow; use crate::state::AuditState; @@ -14,11 +15,15 @@ audit_meta!( ); impl Audit for DangerousTriggers { - fn new(_state: &AuditState<'_>) -> Result { + fn new(_state: &AuditState) -> Result { Ok(Self) } - fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> Result>> { + fn audit_workflow<'doc>( + &self, + workflow: &'doc Workflow, + _config: &Config, + ) -> Result>> { let mut findings = vec![]; if workflow.has_pull_request_target() { findings.push( diff --git a/crates/zizmor/src/audit/excessive_permissions.rs b/crates/zizmor/src/audit/excessive_permissions.rs index 1f2d691e..e01dea29 100644 --- a/crates/zizmor/src/audit/excessive_permissions.rs +++ b/crates/zizmor/src/audit/excessive_permissions.rs @@ -40,7 +40,7 @@ audit_meta!( pub(crate) struct ExcessivePermissions; impl Audit for ExcessivePermissions { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -50,6 +50,7 @@ impl Audit for ExcessivePermissions { fn audit_workflow<'doc>( &self, workflow: &'doc crate::models::workflow::Workflow, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/forbidden_uses.rs b/crates/zizmor/src/audit/forbidden_uses.rs index 5fbc481c..1e1c4723 100644 --- a/crates/zizmor/src/audit/forbidden_uses.rs +++ b/crates/zizmor/src/audit/forbidden_uses.rs @@ -1,20 +1,16 @@ -use anyhow::{Context, anyhow}; use github_actions_models::common::Uses; use super::{Audit, AuditLoadError, AuditState, audit_meta}; +use crate::config::{Config, ForbiddenUsesConfig}; use crate::finding::{Confidence, Finding, Persona, Severity}; -use crate::models::uses::RepositoryUsesPattern; use crate::models::{StepCommon, action::CompositeStep, workflow::Step}; -use serde::Deserialize; -pub(crate) struct ForbiddenUses { - config: ForbiddenUsesConfig, -} +pub(crate) struct ForbiddenUses; audit_meta!(ForbiddenUses, "forbidden-uses", "forbidden action used"); impl ForbiddenUses { - fn use_denied(&self, uses: &Uses) -> bool { + fn use_denied(&self, uses: &Uses, config: &ForbiddenUsesConfig) -> bool { match uses { // Local uses are never denied. Uses::Local(_) => false, @@ -25,7 +21,7 @@ impl ForbiddenUses { tracing::warn!("can't evaluate direct Docker uses"); false } - Uses::Repository(uses) => match &self.config { + Uses::Repository(uses) => match config { ForbiddenUsesConfig::Allow { allow } => { !allow.iter().any(|pattern| pattern.matches(uses)) } @@ -39,14 +35,20 @@ impl ForbiddenUses { fn process_step<'doc>( &self, step: &impl StepCommon<'doc>, + config: &Config, ) -> anyhow::Result>> { let mut findings = vec![]; + let Some(config) = config.forbidden_uses_config.as_ref() else { + tracing::trace!("no forbidden-uses config for this input; skipping"); + return Ok(findings); + }; + let Some(uses) = step.uses() else { return Ok(findings); }; - if self.use_denied(uses) { + if self.use_denied(uses, config) { findings.push( Self::finding() .confidence(Confidence::High) @@ -67,37 +69,26 @@ impl ForbiddenUses { } impl Audit for ForbiddenUses { - fn new(state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { - let Some(config) = state - .config - .rule_config(Self::ident()) - .context("invalid configuration") - .map_err(AuditLoadError::Fail)? - else { - return Err(AuditLoadError::Skip(anyhow!("audit not configured"))); - }; - - Ok(Self { config }) + Ok(Self) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result>> { - self.process_step(step) + fn audit_step<'doc>( + &self, + step: &Step<'doc>, + config: &Config, + ) -> anyhow::Result>> { + self.process_step(step, config) } fn audit_composite_step<'a>( &self, step: &CompositeStep<'a>, + config: &Config, ) -> anyhow::Result>> { - self.process_step(step) + self.process_step(step, config) } } - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case", untagged)] -enum ForbiddenUsesConfig { - Allow { allow: Vec }, - Deny { deny: Vec }, -} diff --git a/crates/zizmor/src/audit/github_env.rs b/crates/zizmor/src/audit/github_env.rs index 07b1300a..8dc2ca83 100644 --- a/crates/zizmor/src/audit/github_env.rs +++ b/crates/zizmor/src/audit/github_env.rs @@ -10,6 +10,7 @@ use tree_sitter::{ }; use super::{Audit, AuditLoadError, audit_meta}; +use crate::config::Config; use crate::finding::location::Locatable as _; use crate::finding::{Confidence, Finding, Severity}; use crate::models::{workflow::JobExt as _, workflow::Step}; @@ -316,7 +317,7 @@ impl GitHubEnv { } impl Audit for GitHubEnv { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -344,7 +345,11 @@ impl Audit for GitHubEnv { }) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result>> { + fn audit_step<'doc>( + &self, + step: &Step<'doc>, + _config: &Config, + ) -> anyhow::Result>> { let mut findings = vec![]; let workflow = step.workflow(); @@ -395,6 +400,7 @@ impl Audit for GitHubEnv { fn audit_composite_step<'doc>( &self, step: &super::CompositeStep<'doc>, + _config: &Config, ) -> Result>> { let mut findings = vec![]; @@ -426,7 +432,6 @@ impl Audit for GitHubEnv { mod tests { use crate::audit::Audit; use crate::audit::github_env::{GITHUB_ENV_WRITE_CMD, GitHubEnv}; - use crate::github_api::GitHubHost; use crate::state::AuditState; #[test] @@ -491,12 +496,7 @@ mod tests { ("echo 'completely-static' \"foo\" >> $GITHUB_ENV", false), // LHS is completely static ("echo \"completely-static\" >> $GITHUB_ENV", false), // LHS is completely static ] { - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let sut = GitHubEnv::new(&audit_state).expect("failed to create audit"); @@ -607,12 +607,7 @@ mod tests { false, ), // GITHUB_ENV is not a variable ] { - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let sut = GitHubEnv::new(&audit_state).expect("failed to create audit"); diff --git a/crates/zizmor/src/audit/hardcoded_container_credentials.rs b/crates/zizmor/src/audit/hardcoded_container_credentials.rs index 0a81af13..86f27b13 100644 --- a/crates/zizmor/src/audit/hardcoded_container_credentials.rs +++ b/crates/zizmor/src/audit/hardcoded_container_credentials.rs @@ -18,7 +18,7 @@ audit_meta!( ); impl Audit for HardcodedContainerCredentials { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -28,6 +28,7 @@ impl Audit for HardcodedContainerCredentials { fn audit_workflow<'doc>( &self, workflow: &'doc crate::models::workflow::Workflow, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/impostor_commit.rs b/crates/zizmor/src/audit/impostor_commit.rs index ab111fc1..c4fc598d 100644 --- a/crates/zizmor/src/audit/impostor_commit.rs +++ b/crates/zizmor/src/audit/impostor_commit.rs @@ -10,6 +10,7 @@ use github_actions_models::common::{RepositoryUses, Uses}; use super::{Audit, AuditLoadError, Job, audit_meta}; use crate::{ + config::Config, finding::{Confidence, Finding, Severity, location::Locatable as _}, github_api::{self, ComparisonStatus}, models::{StepCommon, uses::RepositoryUsesExt as _, workflow::Workflow}, @@ -113,7 +114,7 @@ impl ImpostorCommit { } impl Audit for ImpostorCommit { - fn new(state: &AuditState<'_>) -> Result { + fn new(state: &AuditState) -> Result { if state.no_online_audits { return Err(AuditLoadError::Skip(anyhow!( "offline audits only requested" @@ -127,7 +128,11 @@ impl Audit for ImpostorCommit { .map(|client| ImpostorCommit { client }) } - fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> Result>> { + fn audit_workflow<'doc>( + &self, + workflow: &'doc Workflow, + _config: &Config, + ) -> Result>> { let mut findings = vec![]; for job in workflow.jobs() { @@ -179,6 +184,7 @@ impl Audit for ImpostorCommit { fn audit_composite_step<'a>( &self, step: &super::CompositeStep<'a>, + _config: &Config, ) -> Result>> { let mut findings = vec![]; let Some(Uses::Repository(uses)) = step.uses() else { diff --git a/crates/zizmor/src/audit/insecure_commands.rs b/crates/zizmor/src/audit/insecure_commands.rs index c52f93b4..32c3ce69 100644 --- a/crates/zizmor/src/audit/insecure_commands.rs +++ b/crates/zizmor/src/audit/insecure_commands.rs @@ -9,6 +9,7 @@ use yamlpatch::{Op, Patch}; use super::{AuditLoadError, Job, audit_meta}; use crate::audit::Audit; +use crate::config::Config; use crate::finding::location::Locatable as _; use crate::finding::{ Confidence, Finding, Fix, FixDisposition, Persona, Severity, location::SymbolicLocation, @@ -117,14 +118,18 @@ impl InsecureCommands { } impl Audit for InsecureCommands { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { Ok(Self) } - fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> anyhow::Result>> { + fn audit_workflow<'doc>( + &self, + workflow: &'doc Workflow, + _config: &Config, + ) -> anyhow::Result>> { let mut results = vec![]; match &workflow.env { @@ -161,6 +166,7 @@ impl Audit for InsecureCommands { fn audit_composite_step<'doc>( &self, step: &super::CompositeStep<'doc>, + _config: &Config, ) -> Result>> { let mut findings = vec![]; @@ -187,7 +193,7 @@ impl Audit for InsecureCommands { mod tests { use super::*; use crate::{ - github_api::GitHubHost, + config::Config, models::{AsDocument, workflow::Workflow}, registry::input::InputKey, state::AuditState, @@ -198,14 +204,9 @@ mod tests { ($audit_type:ty, $filename:expr, $workflow_content:expr, $test_fn:expr) => {{ let key = InputKey::local("fakegroup".into(), $filename, None::<&str>).unwrap(); let workflow = Workflow::from_string($workflow_content.to_string(), key).unwrap(); - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let audit = <$audit_type>::new(&audit_state).unwrap(); - let findings = audit.audit_workflow(&workflow).unwrap(); + let findings = audit.audit_workflow(&workflow, &Config::default()).unwrap(); $test_fn(&workflow, findings) }}; diff --git a/crates/zizmor/src/audit/known_vulnerable_actions.rs b/crates/zizmor/src/audit/known_vulnerable_actions.rs index 84731318..d4139ec8 100644 --- a/crates/zizmor/src/audit/known_vulnerable_actions.rs +++ b/crates/zizmor/src/audit/known_vulnerable_actions.rs @@ -10,6 +10,7 @@ use github_actions_models::common::{RepositoryUses, Uses}; use super::{Audit, AuditLoadError, audit_meta}; use crate::{ + config::Config, finding::{Confidence, Finding, Fix, Severity, location::Routable as _}, github_api, models::{StepCommon, action::CompositeStep, uses::RepositoryUsesExt as _, workflow::Step}, @@ -256,7 +257,7 @@ impl KnownVulnerableActions { } impl Audit for KnownVulnerableActions { - fn new(state: &AuditState<'_>) -> Result + fn new(state: &AuditState) -> Result where Self: Sized, { @@ -273,19 +274,21 @@ impl Audit for KnownVulnerableActions { .map(|client| KnownVulnerableActions { client }) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> Result>> { + fn audit_step<'doc>(&self, step: &Step<'doc>, _config: &Config) -> Result>> { self.process_step(step) } - fn audit_composite_step<'doc>(&self, step: &CompositeStep<'doc>) -> Result>> { + fn audit_composite_step<'doc>( + &self, + step: &CompositeStep<'doc>, + _config: &Config, + ) -> Result>> { self.process_step(step) } } #[cfg(test)] mod tests { - use std::path::Path; - use insta::assert_snapshot; use super::*; @@ -296,20 +299,17 @@ mod tests { // Helper function to create a test KnownVulnerableActions instance fn create_test_audit() -> KnownVulnerableActions { - let config = crate::config::Config::default(); - let state = crate::state::AuditState { - config: &config, - no_online_audits: false, - gh_client: Some( + let state = crate::state::AuditState::new( + false, + Some( github_api::Client::new( - &github_api::GitHubHost::Standard("github.com".to_string()), - &github_api::GitHubToken::new("fake").unwrap(), - Path::new("/tmp"), + github_api::GitHubHost::default(), + github_api::GitHubToken::new("fake").unwrap(), + "/tmp".into(), ) .unwrap(), ), - gh_hostname: crate::github_api::GitHubHost::Standard("github.com".to_string()), - }; + ); KnownVulnerableActions::new(&state).unwrap() } @@ -718,13 +718,7 @@ jobs: #[test] fn test_offline_audit_state_creation() { // Test that we can create an audit state without a GitHub token - let config = crate::config::Config::default(); - let state = crate::state::AuditState { - config: &config, - no_online_audits: true, - gh_client: None, - gh_hostname: crate::github_api::GitHubHost::Standard("github.com".to_string()), - }; + let state = crate::state::AuditState::default(); // This should fail because no GitHub token is provided let audit_result = KnownVulnerableActions::new(&state); @@ -750,25 +744,22 @@ jobs: let key = InputKey::local("fakegroup".into(), "dummy.yml", None::<&str>).unwrap(); let workflow = Workflow::from_string(workflow_content.to_string(), key).unwrap(); - let config = crate::config::Config::default(); - let state = crate::state::AuditState { - config: &config, - no_online_audits: false, - gh_client: Some( + let state = crate::state::AuditState::new( + false, + Some( github_api::Client::new( - &github_api::GitHubHost::Standard("github.com".to_string()), - &github_api::GitHubToken::new(&std::env::var("GH_TOKEN").unwrap()).unwrap(), - Path::new("/tmp"), + github_api::GitHubHost::default(), + github_api::GitHubToken::new(&std::env::var("GH_TOKEN").unwrap()).unwrap(), + "/tmp".into(), ) .unwrap(), ), - gh_hostname: crate::github_api::GitHubHost::Standard("github.com".to_string()), - }; + ); let audit = KnownVulnerableActions::new(&state).unwrap(); let input = workflow.into(); - let findings = audit.audit(&input).unwrap(); + let findings = audit.audit(&input, &Config::default()).unwrap(); assert_eq!(findings.len(), 1); let new_doc = findings[0].fixes[0].apply(input.as_document()).unwrap(); @@ -806,25 +797,22 @@ jobs: let key = InputKey::local("fakegroup".into(), "dummy.yml", None::<&str>).unwrap(); let workflow = Workflow::from_string(workflow_content.to_string(), key).unwrap(); - let config = crate::config::Config::default(); - let state = crate::state::AuditState { - config: &config, - no_online_audits: false, - gh_client: Some( + let state = crate::state::AuditState::new( + false, + Some( github_api::Client::new( - &github_api::GitHubHost::Standard("github.com".to_string()), - &github_api::GitHubToken::new(&std::env::var("GH_TOKEN").unwrap()).unwrap(), - Path::new("/tmp"), + github_api::GitHubHost::default(), + github_api::GitHubToken::new(&std::env::var("GH_TOKEN").unwrap()).unwrap(), + "/tmp".into(), ) .unwrap(), ), - gh_hostname: crate::github_api::GitHubHost::Standard("github.com".to_string()), - }; + ); let audit = KnownVulnerableActions::new(&state).unwrap(); let input = workflow.into(); - let findings = audit.audit(&input).unwrap(); + let findings = audit.audit(&input, &Config::default()).unwrap(); assert_eq!(findings.len(), 1); let new_doc = findings[0].fixes[0].apply(input.as_document()).unwrap(); diff --git a/crates/zizmor/src/audit/mod.rs b/crates/zizmor/src/audit/mod.rs index e3324d47..d7a320c9 100644 --- a/crates/zizmor/src/audit/mod.rs +++ b/crates/zizmor/src/audit/mod.rs @@ -6,10 +6,12 @@ use tracing::instrument; use yamlpath::Document; use crate::{ + config::Config, finding::{Finding, FindingBuilder, location::SymbolicLocation}, models::{ - AsDocument, action::Action, action::CompositeStep, workflow::Job, workflow::NormalJob, - workflow::ReusableWorkflowCallJob, workflow::Step, workflow::Workflow, + AsDocument, + action::{Action, CompositeStep}, + workflow::{Job, NormalJob, ReusableWorkflowCallJob, Step, Workflow}, }, registry::input::InputKey, state::AuditState, @@ -197,18 +199,26 @@ pub(crate) enum AuditLoadError { /// **only** [`Audit::audit`] and not [`Audit::audit_normal_job`] or /// [`Audit::audit_step`]. pub(crate) trait Audit: AuditCore { - fn new(state: &AuditState<'_>) -> Result + fn new(state: &AuditState) -> Result where Self: Sized; - fn audit_step<'doc>(&self, _step: &Step<'doc>) -> anyhow::Result>> { + fn audit_step<'doc>( + &self, + _step: &Step<'doc>, + _config: &Config, + ) -> anyhow::Result>> { Ok(vec![]) } - fn audit_normal_job<'doc>(&self, job: &NormalJob<'doc>) -> anyhow::Result>> { + fn audit_normal_job<'doc>( + &self, + job: &NormalJob<'doc>, + config: &Config, + ) -> anyhow::Result>> { let mut results = vec![]; for step in job.steps() { - results.extend(self.audit_step(&step)?); + results.extend(self.audit_step(&step, config)?); } Ok(results) } @@ -216,20 +226,25 @@ pub(crate) trait Audit: AuditCore { fn audit_reusable_job<'doc>( &self, _job: &ReusableWorkflowCallJob<'doc>, + _config: &Config, ) -> anyhow::Result>> { Ok(vec![]) } - fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> anyhow::Result>> { + fn audit_workflow<'doc>( + &self, + workflow: &'doc Workflow, + config: &Config, + ) -> anyhow::Result>> { let mut results = vec![]; for job in workflow.jobs() { match job { Job::NormalJob(normal) => { - results.extend(self.audit_normal_job(&normal)?); + results.extend(self.audit_normal_job(&normal, config)?); } Job::ReusableWorkflowCallJob(reusable) => { - results.extend(self.audit_reusable_job(&reusable)?); + results.extend(self.audit_reusable_job(&reusable, config)?); } } } @@ -240,23 +255,32 @@ pub(crate) trait Audit: AuditCore { fn audit_composite_step<'doc>( &self, _step: &CompositeStep<'doc>, + _config: &Config, ) -> anyhow::Result>> { Ok(vec![]) } - fn audit_action<'doc>(&self, action: &'doc Action) -> anyhow::Result>> { + fn audit_action<'doc>( + &self, + action: &'doc Action, + config: &Config, + ) -> anyhow::Result>> { let mut results = vec![]; if let Some(steps) = action.steps() { for step in steps { - results.extend(self.audit_composite_step(&step)?); + results.extend(self.audit_composite_step(&step, config)?); } } Ok(results) } - fn audit_raw<'doc>(&self, _input: &'doc AuditInput) -> anyhow::Result>> { + fn audit_raw<'doc>( + &self, + _input: &'doc AuditInput, + _config: &Config, + ) -> anyhow::Result>> { Ok(vec![]) } @@ -264,14 +288,18 @@ pub(crate) trait Audit: AuditCore { /// /// Implementors **should not** override this blanket implementation, /// since it's marked with tracing instrumentation. - #[instrument(skip(self))] - fn audit<'doc>(&self, input: &'doc AuditInput) -> anyhow::Result>> { + #[instrument(skip(self, config))] + fn audit<'doc>( + &self, + input: &'doc AuditInput, + config: &Config, + ) -> anyhow::Result>> { let mut results = match input { - AuditInput::Workflow(workflow) => self.audit_workflow(workflow), - AuditInput::Action(action) => self.audit_action(action), + AuditInput::Workflow(workflow) => self.audit_workflow(workflow, config), + AuditInput::Action(action) => self.audit_action(action, config), }?; - results.extend(self.audit_raw(input)?); + results.extend(self.audit_raw(input, config)?); Ok(results) } diff --git a/crates/zizmor/src/audit/obfuscation.rs b/crates/zizmor/src/audit/obfuscation.rs index 3ec67b14..2e32aa78 100644 --- a/crates/zizmor/src/audit/obfuscation.rs +++ b/crates/zizmor/src/audit/obfuscation.rs @@ -3,6 +3,7 @@ use github_actions_models::common::{RepositoryUses, Uses}; use crate::{ Confidence, Severity, + config::Config, finding::{ Finding, Persona, location::{Feature, Location}, @@ -124,14 +125,18 @@ impl Obfuscation { } impl Audit for Obfuscation { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { Ok(Self) } - fn audit_raw<'doc>(&self, input: &'doc AuditInput) -> anyhow::Result>> { + fn audit_raw<'doc>( + &self, + input: &'doc AuditInput, + _config: &Config, + ) -> anyhow::Result>> { let mut findings = vec![]; for (expr, expr_span) in parse_fenced_expressions_from_input(input) { @@ -161,13 +166,18 @@ impl Audit for Obfuscation { Ok(findings) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result>> { + fn audit_step<'doc>( + &self, + step: &Step<'doc>, + _config: &Config, + ) -> anyhow::Result>> { self.process_step(step) } fn audit_composite_step<'a>( &self, step: &CompositeStep<'a>, + _config: &Config, ) -> anyhow::Result>> { self.process_step(step) } diff --git a/crates/zizmor/src/audit/overprovisioned_secrets.rs b/crates/zizmor/src/audit/overprovisioned_secrets.rs index 82d54f59..901cdffb 100644 --- a/crates/zizmor/src/audit/overprovisioned_secrets.rs +++ b/crates/zizmor/src/audit/overprovisioned_secrets.rs @@ -31,6 +31,7 @@ impl Audit for OverprovisionedSecrets { fn audit_raw<'doc>( &self, input: &'doc AuditInput, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/ref_confusion.rs b/crates/zizmor/src/audit/ref_confusion.rs index 1bfad408..e569a4ae 100644 --- a/crates/zizmor/src/audit/ref_confusion.rs +++ b/crates/zizmor/src/audit/ref_confusion.rs @@ -49,7 +49,7 @@ impl RefConfusion { } impl Audit for RefConfusion { - fn new(state: &AuditState<'_>) -> Result + fn new(state: &AuditState) -> Result where Self: Sized, { @@ -69,6 +69,7 @@ impl Audit for RefConfusion { fn audit_workflow<'doc>( &self, workflow: &'doc crate::models::workflow::Workflow, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; @@ -122,7 +123,11 @@ impl Audit for RefConfusion { Ok(findings) } - fn audit_composite_step<'a>(&self, step: &CompositeStep<'a>) -> Result>> { + fn audit_composite_step<'a>( + &self, + step: &CompositeStep<'a>, + _config: &crate::config::Config, + ) -> Result>> { let mut findings = vec![]; let Some(Uses::Repository(uses)) = step.uses() else { diff --git a/crates/zizmor/src/audit/secrets_inherit.rs b/crates/zizmor/src/audit/secrets_inherit.rs index 9b361161..e0e1c4ea 100644 --- a/crates/zizmor/src/audit/secrets_inherit.rs +++ b/crates/zizmor/src/audit/secrets_inherit.rs @@ -15,7 +15,7 @@ audit_meta!( ); impl Audit for SecretsInherit { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -25,6 +25,7 @@ impl Audit for SecretsInherit { fn audit_reusable_job<'doc>( &self, job: &super::ReusableWorkflowCallJob<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/self_hosted_runner.rs b/crates/zizmor/src/audit/self_hosted_runner.rs index 2059b385..648086e0 100644 --- a/crates/zizmor/src/audit/self_hosted_runner.rs +++ b/crates/zizmor/src/audit/self_hosted_runner.rs @@ -27,7 +27,7 @@ audit_meta!( ); impl Audit for SelfHostedRunner { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -37,6 +37,7 @@ impl Audit for SelfHostedRunner { fn audit_workflow<'doc>( &self, workflow: &'doc crate::models::workflow::Workflow, + _config: &crate::config::Config, ) -> Result>> { let mut results = vec![]; diff --git a/crates/zizmor/src/audit/stale_action_refs.rs b/crates/zizmor/src/audit/stale_action_refs.rs index 6036791c..8d7bb3d4 100644 --- a/crates/zizmor/src/audit/stale_action_refs.rs +++ b/crates/zizmor/src/audit/stale_action_refs.rs @@ -6,6 +6,7 @@ use github_actions_models::common::{RepositoryUses, Uses}; use super::{Audit, AuditLoadError, audit_meta}; use crate::{ Persona, + config::Config, finding::{Confidence, Finding, Severity}, github_api, models::{StepCommon, action::CompositeStep, uses::RepositoryUsesExt as _, workflow::Step}, @@ -57,7 +58,7 @@ impl StaleActionRefs { } impl Audit for StaleActionRefs { - fn new(state: &AuditState<'_>) -> Result + fn new(state: &AuditState) -> Result where Self: Sized, { @@ -74,11 +75,15 @@ impl Audit for StaleActionRefs { .map(|client| StaleActionRefs { client }) } - fn audit_step<'w>(&self, step: &Step<'w>) -> Result>> { + fn audit_step<'w>(&self, step: &Step<'w>, _config: &Config) -> Result>> { self.process_step(step) } - fn audit_composite_step<'a>(&self, step: &CompositeStep<'a>) -> Result>> { + fn audit_composite_step<'a>( + &self, + step: &CompositeStep<'a>, + _config: &Config, + ) -> Result>> { self.process_step(step) } } diff --git a/crates/zizmor/src/audit/template_injection.rs b/crates/zizmor/src/audit/template_injection.rs index 523001c9..1026b81f 100644 --- a/crates/zizmor/src/audit/template_injection.rs +++ b/crates/zizmor/src/audit/template_injection.rs @@ -27,6 +27,7 @@ use itertools::Itertools as _; use super::{Audit, AuditLoadError, audit_meta}; use crate::{ + config::Config, finding::{ Confidence, Finding, Fix, Persona, Severity, location::{Routable as _, SymbolicLocation}, @@ -599,20 +600,25 @@ impl TemplateInjection { } impl Audit for TemplateInjection { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { Ok(Self) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result>> { + fn audit_step<'doc>( + &self, + step: &Step<'doc>, + _config: &Config, + ) -> anyhow::Result>> { self.process_step(step) } fn audit_composite_step<'a>( &self, step: &CompositeStep<'a>, + _config: &Config, ) -> anyhow::Result>> { self.process_step(step) } @@ -624,7 +630,7 @@ mod tests { use crate::audit::Audit; use crate::audit::template_injection::{Capability, TemplateInjection}; - use crate::github_api::GitHubHost; + use crate::config::Config; use crate::models::AsDocument; use crate::models::workflow::Workflow; use crate::registry::input::InputKey; @@ -635,14 +641,9 @@ mod tests { ($audit_type:ty, $filename:expr, $workflow_content:expr, $test_fn:expr) => {{ let key = InputKey::local("fakegroup".into(), $filename, None::<&str>).unwrap(); let workflow = Workflow::from_string($workflow_content.to_string(), key).unwrap(); - let audit_state = AuditState { - config: &Default::default(), - no_online_audits: false, - gh_client: None, - gh_hostname: GitHubHost::Standard("github.com".into()), - }; + let audit_state = AuditState::default(); let audit = <$audit_type>::new(&audit_state).unwrap(); - let findings = audit.audit_workflow(&workflow).unwrap(); + let findings = audit.audit_workflow(&workflow, &Config::default()).unwrap(); $test_fn(&workflow, findings) }}; diff --git a/crates/zizmor/src/audit/unpinned_images.rs b/crates/zizmor/src/audit/unpinned_images.rs index fedb838b..721471ec 100644 --- a/crates/zizmor/src/audit/unpinned_images.rs +++ b/crates/zizmor/src/audit/unpinned_images.rs @@ -42,13 +42,14 @@ audit_meta!( ); impl Audit for UnpinnedImages { - fn new(_state: &AuditState<'_>) -> Result { + fn new(_state: &AuditState) -> Result { Ok(Self) } fn audit_normal_job<'doc>( &self, job: &super::NormalJob<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; let mut image_refs_with_locations: Vec<(DockerUses, SymbolicLocation<'doc>)> = vec![]; diff --git a/crates/zizmor/src/audit/unpinned_uses.rs b/crates/zizmor/src/audit/unpinned_uses.rs index 7900950e..d6a7b1c8 100644 --- a/crates/zizmor/src/audit/unpinned_uses.rs +++ b/crates/zizmor/src/audit/unpinned_uses.rs @@ -1,22 +1,21 @@ -use std::collections::HashMap; - -use anyhow::Context; -use github_actions_models::common::{RepositoryUses, Uses}; -use serde::Deserialize; +use github_actions_models::common::Uses; use super::{Audit, AuditLoadError, AuditState, audit_meta}; +use crate::config::{Config, UsesPolicy}; use crate::finding::{Confidence, Finding, Persona, Severity}; use crate::models::uses::RepositoryUsesPattern; use crate::models::{StepCommon, action::CompositeStep, uses::UsesExt as _, workflow::Step}; -pub(crate) struct UnpinnedUses { - policies: UnpinnedUsesPolicies, -} +pub(crate) struct UnpinnedUses; audit_meta!(UnpinnedUses, "unpinned-uses", "unpinned action reference"); impl UnpinnedUses { - pub fn evaluate_pinning(&self, uses: &Uses) -> Option<(String, Severity, Persona)> { + pub fn evaluate_pinning( + &self, + uses: &Uses, + config: &Config, + ) -> Option<(String, Severity, Persona)> { match uses { // Don't evaluate pinning for local `uses:`, since unpinned references // are fully controlled by the repository anyways. @@ -46,7 +45,7 @@ impl UnpinnedUses { } } Uses::Repository(repo_uses) => { - let (pattern, policy) = self.policies.get_policy(repo_uses); + let (pattern, policy) = config.unpinned_uses_policies.get_policy(repo_uses); let pat_desc = match pattern { Some(RepositoryUsesPattern::Any) | None => "blanket".into(), @@ -90,6 +89,7 @@ impl UnpinnedUses { fn process_step<'doc>( &self, step: &impl StepCommon<'doc>, + config: &Config, ) -> anyhow::Result>> { let mut findings = vec![]; @@ -97,7 +97,7 @@ impl UnpinnedUses { return Ok(findings); }; - if let Some((annotation, severity, persona)) = self.evaluate_pinning(uses) { + if let Some((annotation, severity, persona)) = self.evaluate_pinning(uses, config) { findings.push( Self::finding() .confidence(Confidence::High) @@ -118,184 +118,26 @@ impl UnpinnedUses { } impl Audit for UnpinnedUses { - fn new(state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { - let config = state - .config - .rule_config::(Self::ident()) - .context("invalid configuration") - .map_err(AuditLoadError::Fail)? - .unwrap_or_default(); - - let policies = UnpinnedUsesPolicies::try_from(config) - .context("invalid configuration") - .map_err(AuditLoadError::Fail)?; - - Ok(Self { policies }) + Ok(Self) } - fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result>> { - self.process_step(step) + fn audit_step<'doc>( + &self, + step: &Step<'doc>, + config: &Config, + ) -> anyhow::Result>> { + self.process_step(step, config) } fn audit_composite_step<'a>( &self, step: &CompositeStep<'a>, + config: &Config, ) -> anyhow::Result>> { - self.process_step(step) - } -} - -/// Config for the `unpinned-uses` rule. -/// -/// This configuration is reified into an `UnpinnedUsesPolicies`. -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] -struct UnpinnedUsesConfig { - /// A mapping of `uses:` patterns to policies. - policies: HashMap, -} - -impl Default for UnpinnedUsesConfig { - fn default() -> Self { - Self { - policies: [ - ( - RepositoryUsesPattern::InOwner("actions".into()), - UsesPolicy::RefPin, - ), - ( - RepositoryUsesPattern::InOwner("github".into()), - UsesPolicy::RefPin, - ), - ( - RepositoryUsesPattern::InOwner("dependabot".into()), - UsesPolicy::RefPin, - ), - (RepositoryUsesPattern::Any, UsesPolicy::HashPin), - ] - .into(), - } - } -} - -/// A singular policy for a `uses:` reference. -#[derive(Copy, Clone, Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -enum UsesPolicy { - /// No policy; all `uses:` references are allowed, even unpinned ones. - Any, - /// `uses:` references must be pinned to a tag, branch, or hash ref. - RefPin, - /// `uses:` references must be pinned to a hash ref. - HashPin, -} - -/// Represents the set of policies used to evaluate `uses:` references. -struct UnpinnedUsesPolicies { - /// The policy tree is a mapping of `owner` slugs to a list of - /// `(pattern, policy)` pairs under that owner, ordered by specificity. - /// - /// For example, a config containing both `foo/*: hash-pin` and - /// `foo/bar: ref-pin` would produce a policy tree like this: - /// - /// ```text - /// foo: - /// - foo/bar: ref-pin - /// - foo/*: hash-pin - /// ``` - /// - /// This is done for performance reasons: a two-level structure here - /// means that checking a `uses:` is a linear scan of the policies - /// for that owner, rather than a full scan of all policies. - policy_tree: HashMap>, - - /// This is the policy that's applied if nothing in the policy tree matches. - /// - /// Normally is this configured by an `*` entry in the config or by - /// `UnpinnedUsesConfig::default()`. However, if the user explicitly - /// omits a `*` rule, this will be `UsesPolicy::HashPin`. - default_policy: UsesPolicy, -} - -impl UnpinnedUsesPolicies { - /// Returns the most specific policy for the given repository `uses` reference, - /// or the default policy if none match. - fn get_policy(&self, uses: &RepositoryUses) -> (Option<&RepositoryUsesPattern>, UsesPolicy) { - match self.policy_tree.get(&uses.owner) { - Some(policies) => { - // Policies are ordered by specificity, so we can - // iterate and return eagerly. - for (uses_pattern, policy) in policies { - if uses_pattern.matches(uses) { - return (Some(uses_pattern), *policy); - } - } - // The policies under `owner/` might be fully divergent - // if there isn't an `owner/*` rule, so we fall back - // to the default policy. - (None, self.default_policy) - } - None => (None, self.default_policy), - } - } -} - -impl TryFrom for UnpinnedUsesPolicies { - type Error = anyhow::Error; - - fn try_from(config: UnpinnedUsesConfig) -> Result { - let mut policy_tree: HashMap> = - HashMap::new(); - let mut default_policy = UsesPolicy::HashPin; - - for (pattern, policy) in config.policies { - match pattern { - // Patterns with refs don't make sense in this context, since - // we're establishing policies for the refs themselves. - RepositoryUsesPattern::ExactWithRef { .. } => { - return Err(anyhow::anyhow!("can't use exact ref patterns here")); - } - RepositoryUsesPattern::ExactPath { ref owner, .. } => { - policy_tree - .entry(owner.clone()) - .or_default() - .push((pattern, policy)); - } - RepositoryUsesPattern::ExactRepo { ref owner, .. } => { - policy_tree - .entry(owner.clone()) - .or_default() - .push((pattern, policy)); - } - RepositoryUsesPattern::InRepo { ref owner, .. } => { - policy_tree - .entry(owner.clone()) - .or_default() - .push((pattern, policy)); - } - RepositoryUsesPattern::InOwner(ref owner) => { - policy_tree - .entry(owner.clone()) - .or_default() - .push((pattern, policy)); - } - RepositoryUsesPattern::Any => { - default_policy = policy; - } - } - } - - // Sort the policies for each owner by specificity. - for policies in policy_tree.values_mut() { - policies.sort_by(|a, b| a.0.cmp(&b.0)); - } - - Ok(Self { - policy_tree, - default_policy, - }) + self.process_step(step, config) } } diff --git a/crates/zizmor/src/audit/unredacted_secrets.rs b/crates/zizmor/src/audit/unredacted_secrets.rs index 7aae721e..0e976613 100644 --- a/crates/zizmor/src/audit/unredacted_secrets.rs +++ b/crates/zizmor/src/audit/unredacted_secrets.rs @@ -19,7 +19,7 @@ audit_meta!( ); impl Audit for UnredactedSecrets { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -29,6 +29,7 @@ impl Audit for UnredactedSecrets { fn audit_raw<'doc>( &self, input: &'doc super::AuditInput, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = vec![]; diff --git a/crates/zizmor/src/audit/unsound_condition.rs b/crates/zizmor/src/audit/unsound_condition.rs index 3ae7a909..f31fb6b7 100644 --- a/crates/zizmor/src/audit/unsound_condition.rs +++ b/crates/zizmor/src/audit/unsound_condition.rs @@ -91,7 +91,7 @@ impl UnsoundCondition { } impl Audit for UnsoundCondition { - fn new(_state: &crate::state::AuditState<'_>) -> Result + fn new(_state: &crate::state::AuditState) -> Result where Self: Sized, { @@ -101,6 +101,7 @@ impl Audit for UnsoundCondition { fn audit_normal_job<'doc>( &self, job: &crate::models::workflow::NormalJob<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { self.process_conditions(job.parent(), job.conditions()) } @@ -108,6 +109,7 @@ impl Audit for UnsoundCondition { fn audit_reusable_job<'doc>( &self, job: &crate::models::workflow::ReusableWorkflowCallJob<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let conds = job.r#if.iter().map(|cond| (cond, job.location())); self.process_conditions(job.parent(), conds) @@ -116,6 +118,7 @@ impl Audit for UnsoundCondition { fn audit_action<'doc>( &self, action: &'doc crate::models::action::Action, + _config: &crate::config::Config, ) -> anyhow::Result>> { self.process_conditions(action, action.conditions()) } diff --git a/crates/zizmor/src/audit/unsound_contains.rs b/crates/zizmor/src/audit/unsound_contains.rs index 9599d1a1..762ed40c 100644 --- a/crates/zizmor/src/audit/unsound_contains.rs +++ b/crates/zizmor/src/audit/unsound_contains.rs @@ -33,7 +33,7 @@ audit_meta!( ); impl Audit for UnsoundContains { - fn new(_state: &AuditState<'_>) -> Result + fn new(_state: &AuditState) -> Result where Self: Sized, { @@ -43,6 +43,7 @@ impl Audit for UnsoundContains { fn audit_normal_job<'w>( &self, job: &super::NormalJob<'w>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let conditions = job.conditions().filter_map(|(cond, loc)| { if let If::Expr(expr) = cond { diff --git a/crates/zizmor/src/audit/use_trusted_publishing.rs b/crates/zizmor/src/audit/use_trusted_publishing.rs index 9c5d7323..62ff3ae1 100644 --- a/crates/zizmor/src/audit/use_trusted_publishing.rs +++ b/crates/zizmor/src/audit/use_trusted_publishing.rs @@ -278,7 +278,7 @@ impl UseTrustedPublishing { } impl Audit for UseTrustedPublishing { - fn new(_state: &AuditState<'_>) -> Result { + fn new(_state: &AuditState) -> Result { let bash: Language = tree_sitter_bash::LANGUAGE.into(); let pwsh: Language = tree_sitter_powershell::LANGUAGE.into(); @@ -295,6 +295,7 @@ impl Audit for UseTrustedPublishing { fn audit_step<'doc>( &self, step: &crate::models::workflow::Step<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { let mut findings = self.process_step(step)?; @@ -353,6 +354,7 @@ impl Audit for UseTrustedPublishing { fn audit_composite_step<'doc>( &self, step: &crate::models::action::CompositeStep<'doc>, + _config: &crate::config::Config, ) -> anyhow::Result>> { self.process_step(step) } diff --git a/crates/zizmor/src/config.rs b/crates/zizmor/src/config.rs index df2632df..2572e0b8 100644 --- a/crates/zizmor/src/config.rs +++ b/crates/zizmor/src/config.rs @@ -1,12 +1,24 @@ use std::{collections::HashMap, fs, num::NonZeroUsize, str::FromStr}; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Context, Result, anyhow}; +use camino::Utf8Path; +use github_actions_models::common::RepositoryUses; use serde::{ Deserialize, de::{self, DeserializeOwned}, }; -use crate::{App, finding::Finding}; +use crate::{ + App, CollectionOptions, + audit::{AuditCore, forbidden_uses::ForbiddenUses, unpinned_uses::UnpinnedUses}, + finding::Finding, + github_api::Client, + models::uses::RepositoryUsesPattern, + registry::input::RepoSlug, + tips, +}; + +const CONFIG_CANDIDATES: &[&str] = &[".github/zizmor.yml", "zizmor.yml"]; #[derive(Clone, Debug, PartialEq)] pub(crate) struct WorkflowRule { @@ -74,53 +86,406 @@ pub(crate) struct AuditRuleConfig { config: Option, } -/// Runtime configuration, corresponding to a `zizmor.yml` file. +/// Data model for zizmor's configuration file. +/// +/// This is a "raw" representation that matches exactly what +/// we parse from a `zizmor.yml` file. #[derive(Clone, Debug, Default, Deserialize)] #[serde(deny_unknown_fields)] -pub(crate) struct Config { +struct RawConfig { rules: HashMap, } -impl Config { - pub(crate) fn new(app: &App) -> Result { - if app.no_config { - return Ok(Self::default()); - } +impl RawConfig { + fn load(contents: &str) -> Result { + serde_yaml::from_str(contents).map_err(|e| { + anyhow!(tips( + format!("failed to load config: {e:#}"), + &[ + "check your configuration file for errors", + "see: https://docs.zizmor.sh/configuration/" + ] + )) + }) + } - let config = match &app.config { - Some(path) => serde_yaml::from_str(&fs::read_to_string(path)?)?, - None => { - // If the user didn't pass a config path explicitly with - // `--config`, then we attempt to discover one relative to $CWD - // Our procedure is to first look for `$CWD/.github/zizmor.yml`, - // then `$CWD/zizmor.yml`, and then bail. - let cwd = std::env::current_dir() - .with_context(|| "config discovery couldn't access CWD")?; + fn rule_config(&self, ident: &str) -> Result> + where + T: DeserializeOwned, + { + Ok(self + .rules + .get(ident) + .and_then(|rule_config| rule_config.config.as_ref()) + .map(|policy| serde_yaml::from_value::(serde_yaml::Value::Mapping(policy.clone()))) + .transpose()?) + } +} - let path = cwd.join(".github").join("zizmor.yml"); - if path.is_file() { - serde_yaml::from_str(&fs::read_to_string(path)?)? - } else { - let path = cwd.join("zizmor.yml"); - if path.is_file() { - serde_yaml::from_str(&fs::read_to_string(path)?)? - } else { - tracing::debug!("no config discovered; loading default"); - Config::default() +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case", untagged)] +pub(crate) enum ForbiddenUsesConfig { + Allow { allow: Vec }, + Deny { deny: Vec }, +} + +/// Config for the `unpinned-uses` rule. +/// +/// This configuration is reified into an `UnpinnedUsesPolicies`. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +pub(crate) struct UnpinnedUsesConfig { + /// A mapping of `uses:` patterns to policies. + policies: HashMap, +} + +/// A singular policy for a `uses:` reference. +#[derive(Copy, Clone, Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum UsesPolicy { + /// No policy; all `uses:` references are allowed, even unpinned ones. + Any, + /// `uses:` references must be pinned to a tag, branch, or hash ref. + RefPin, + /// `uses:` references must be pinned to a hash ref. + HashPin, +} + +/// Represents the set of policies used to evaluate `uses:` references. +#[derive(Clone, Debug)] +pub(crate) struct UnpinnedUsesPolicies { + /// The policy tree is a mapping of `owner` slugs to a list of + /// `(pattern, policy)` pairs under that owner, ordered by specificity. + /// + /// For example, a config containing both `foo/*: hash-pin` and + /// `foo/bar: ref-pin` would produce a policy tree like this: + /// + /// ```text + /// foo: + /// - foo/bar: ref-pin + /// - foo/*: hash-pin + /// ``` + /// + /// This is done for performance reasons: a two-level structure here + /// means that checking a `uses:` is a linear scan of the policies + /// for that owner, rather than a full scan of all policies. + policy_tree: HashMap>, + + /// This is the policy that's applied if nothing in the policy tree matches. + /// + /// Normally is this configured by an `*` entry in the config or by + /// `UnpinnedUsesConfig::default()`. However, if the user explicitly + /// omits a `*` rule, this will be `UsesPolicy::HashPin`. + default_policy: UsesPolicy, +} + +impl UnpinnedUsesPolicies { + /// Returns the most specific policy for the given repository `uses` reference, + /// or the default policy if none match. + pub(crate) fn get_policy( + &self, + uses: &RepositoryUses, + ) -> (Option<&RepositoryUsesPattern>, UsesPolicy) { + match self.policy_tree.get(&uses.owner) { + Some(policies) => { + // Policies are ordered by specificity, so we can + // iterate and return eagerly. + for (uses_pattern, policy) in policies { + if uses_pattern.matches(uses) { + return (Some(uses_pattern), *policy); } } + // The policies under `owner/` might be fully divergent + // if there isn't an `owner/*` rule, so we fall back + // to the default policy. + (None, self.default_policy) + } + None => (None, self.default_policy), + } + } +} + +impl Default for UnpinnedUsesPolicies { + fn default() -> Self { + Self { + policy_tree: [ + ( + "actions".into(), + vec![( + RepositoryUsesPattern::InOwner("actions".into()), + UsesPolicy::RefPin, + )], + ), + ( + "github".into(), + vec![( + RepositoryUsesPattern::InOwner("github".into()), + UsesPolicy::RefPin, + )], + ), + ( + "dependabot".into(), + vec![( + RepositoryUsesPattern::InOwner("dependabot".into()), + UsesPolicy::RefPin, + )], + ), + ] + .into(), + default_policy: UsesPolicy::HashPin, + } + } +} + +impl TryFrom for UnpinnedUsesPolicies { + type Error = anyhow::Error; + + fn try_from(config: UnpinnedUsesConfig) -> Result { + let mut policy_tree: HashMap> = + HashMap::new(); + let mut default_policy = UsesPolicy::HashPin; + + for (pattern, policy) in config.policies { + match pattern { + // Patterns with refs don't make sense in this context, since + // we're establishing policies for the refs themselves. + RepositoryUsesPattern::ExactWithRef { .. } => { + return Err(anyhow::anyhow!("can't use exact ref patterns here")); + } + RepositoryUsesPattern::ExactPath { ref owner, .. } => { + policy_tree + .entry(owner.clone()) + .or_default() + .push((pattern, policy)); + } + RepositoryUsesPattern::ExactRepo { ref owner, .. } => { + policy_tree + .entry(owner.clone()) + .or_default() + .push((pattern, policy)); + } + RepositoryUsesPattern::InRepo { ref owner, .. } => { + policy_tree + .entry(owner.clone()) + .or_default() + .push((pattern, policy)); + } + RepositoryUsesPattern::InOwner(ref owner) => { + policy_tree + .entry(owner.clone()) + .or_default() + .push((pattern, policy)); + } + RepositoryUsesPattern::Any => { + default_policy = policy; + } + } + } + + // Sort the policies for each owner by specificity. + for policies in policy_tree.values_mut() { + policies.sort_by(|a, b| a.0.cmp(&b.0)); + } + + Ok(Self { + policy_tree, + default_policy, + }) + } +} + +/// zizmor's configuration. +/// +/// This is a wrapper around [`RawConfig`] that pre-computes various +/// audit-specific fields so that failures are caught up-front +/// rather than at audit time. This also saves us some runtime +/// cost by avoiding potentially (very) repetitive deserialization +/// of per-audit configs (K audits * N inputs). +#[derive(Clone, Debug, Default)] +pub(crate) struct Config { + raw: RawConfig, + pub(crate) forbidden_uses_config: Option, + pub(crate) unpinned_uses_policies: UnpinnedUsesPolicies, +} + +impl Config { + /// Loads a [`Config`] from the given contents. + fn load(contents: &str) -> Result { + let raw = RawConfig::load(contents)?; + + let forbidden_uses_config = raw + .rule_config(ForbiddenUses::ident()) + .context("invalid forbidden-uses configuration")?; + + let unpinned_uses_policies = { + if let Some(unpinned_uses_config) = raw + .rule_config::(UnpinnedUses::ident()) + .context("invalid unpinned-uses configuration")? + { + UnpinnedUsesPolicies::try_from(unpinned_uses_config) + .context("invalid unpinned-uses configuration")? + } else { + UnpinnedUsesPolicies::default() } }; - tracing::debug!("loaded config: {config:?}"); + Ok(Self { + raw, + forbidden_uses_config, + unpinned_uses_policies, + }) + } - Ok(config) + /// Discover a [`Config`] according to the collection options. + /// + /// This function models zizmor's current precedence rules for + /// configuration discovery: + /// 1. `--no-config` disables all config loading. + /// 2. `--config ` uses the given config file globally, + /// which we've already loaded into `options.global_config`. + /// 3. Otherwise, we use the provided `discover_fn` to attempt + /// to discover a config file. This function is typically one + /// of [`Config::discover_local`] or [`Config::discover_remote`] + /// depending on the input type. + pub(crate) fn discover(options: &CollectionOptions, discover_fn: F) -> Result + where + F: FnOnce() -> Result>, + { + if options.no_config { + // User has explicitly disabled config loading. + tracing::debug!("skipping config discovery: explicitly disabled"); + Ok(Self::default()) + } else if let Some(config) = &options.global_config { + // The user gave us a (legacy) global config file, + // which takes precedence over any discovered config. + tracing::debug!("config discovery: using global config: {config:?}"); + Ok(config.clone()) + } else { + // Attempt to discover a config file using the provided function. + discover_fn().map(|conf| conf.unwrap_or_default()) + } + } + + /// Discover a [`Config`] in the given directory. + /// + /// This uses the following discovery procedure: + /// 1. If the given directory is `blahblah/.github/workflows/`, + /// start at the parent (i.e. `blahblah/.github/`). Otherwise, start + /// at the given directory. This first directory is the + /// first candidate path. + /// 2. Look for `.github/zizmor.yml` or `zizmor.yml` in the + /// candidate path. If found, load and return it. + /// 3. Otherwise, continue the search in the candidate path's + /// parent directory, repeating step 2, terminating when + /// we reach the filesystem root or the first .git directory. + fn discover_in_dir(path: &Utf8Path) -> Result> { + tracing::debug!("attempting config discovery in `{path}`"); + + let canonical = path.canonicalize_utf8()?; + + let mut candidate_path = if canonical.file_name() == Some("workflows") { + // TODO: Return None here instead of failing? + canonical.parent().ok_or_else(|| { + anyhow!("cannot discover config: no parent directory of `{canonical}`") + })? + } else { + &canonical + }; + + loop { + for candidate in CONFIG_CANDIDATES { + let candidate_path = candidate_path.join(candidate); + if candidate_path.is_file() { + tracing::debug!("found config candidate at `{candidate_path}`"); + return Ok(Some(Self::load(&fs::read_to_string(&candidate_path)?)?)); + } + } + + if candidate_path.join(".git").is_dir() { + tracing::debug!("found `{candidate_path}/.git`, stopping search"); + return Ok(None); + } + + let Some(parent) = candidate_path.parent() else { + tracing::debug!("reached filesystem root without finding a config"); + return Ok(None); + }; + + candidate_path = parent; + } + } + + /// Discover a [`Config`] using rules applicable to the given path. + /// + /// For files, this attempts to walk up the directory tree, + /// looking for either a `zizmor.yml`. + /// The walk starts at the file's grandparent directory. + /// + /// For directories, this attempts to find a `.github/zizmor.yml` or + /// `zizmor.yml` in the directory itself. + pub(crate) fn discover_local(path: &Utf8Path) -> Result> { + tracing::debug!("discovering config for local input `{path}`"); + + if path.is_dir() { + Self::discover_in_dir(path) + } else if path.is_file() { + let Some(parent) = path.parent() else { + tracing::debug!("no parent for {path:?}, cannot discover config"); + return Ok(None); + }; + + Self::discover_in_dir(parent) + } else { + Err(anyhow!( + "cannot discover config for `{path}`: not a file or directory" + )) + } + } + + /// Discover a [`Config`] for a repository slug. + /// + /// This will look for a `.github/zizmor.yml` or `zizmor.yml` + /// in the repository's root directory. + pub(crate) fn discover_remote(client: &Client, slug: &RepoSlug) -> Result> { + let conf = CONFIG_CANDIDATES + .iter() + .find_map(|candidate| client.fetch_single_file(slug, candidate).transpose()) + .map(|contents| { + contents.and_then(|contents| { + tracing::debug!("retrieved config for {slug}"); + Self::load(&contents) + }) + }) + .transpose()?; + + Ok(conf) + } + + /// Loads a global [`Config`] for the given [`App`]. + /// + /// Returns `Ok(None)` unless the user explicitly specifies + /// a config file with `--config`. + pub(crate) fn global(app: &App) -> Result> { + if app.no_config { + Ok(None) + } else if let Some(path) = &app.config { + tracing::debug!("loading config from `{path}`"); + + let contents = fs::read_to_string(path) + .with_context(|| format!("failed to read config file at `{path}`"))?; + + Ok(Some(Self::load(&contents).with_context(|| { + format!("failed to load config file at `{path}`") + })?)) + } else { + Ok(None) + } } /// Returns `true` if this [`Config`] has an ignore rule for the /// given finding. pub(crate) fn ignores(&self, finding: &Finding<'_>) -> bool { - let Some(rule_config) = self.rules.get(finding.ident) else { + let Some(rule_config) = self.raw.rules.get(finding.ident) else { return false; }; @@ -131,6 +496,9 @@ impl Config { // This will hopefully minimize confusion when a finding spans // multiple files, as the first location is the one a user will // typically ignore, suppressing the rest in the process. + // TODO: This needs to filter on something other than filename, + // since that doesn't work for action definitions (which are + // all `action.yml`). for loc in &finding.locations { for rule in ignores .iter() @@ -166,18 +534,6 @@ impl Config { false } - - pub(crate) fn rule_config(&self, ident: &str) -> Result> - where - T: DeserializeOwned, - { - Ok(self - .rules - .get(ident) - .and_then(|rule_config| rule_config.config.as_ref()) - .map(|policy| serde_yaml::from_value::(serde_yaml::Value::Mapping(policy.clone()))) - .transpose()?) - } } #[cfg(test)] diff --git a/crates/zizmor/src/finding.rs b/crates/zizmor/src/finding.rs index aa0759d9..00117d6a 100644 --- a/crates/zizmor/src/finding.rs +++ b/crates/zizmor/src/finding.rs @@ -5,7 +5,7 @@ use clap::ValueEnum; use serde::{Deserialize, Serialize}; use self::location::{Location, SymbolicLocation}; -use crate::{InputKey, models::AsDocument}; +use crate::{InputKey, models::AsDocument, registry::input::Group}; use yamlpatch::{self, Patch}; pub(crate) mod location; @@ -187,6 +187,14 @@ impl Finding<'_> { .find(|l| l.symbolic.is_primary()) .unwrap() } + + /// Return the input group for this finding's primary location. + /// + /// We assume that all locations in a finding belong to the same group, + /// if not the same file within that group. + pub(crate) fn input_group(&self) -> &Group { + self.primary_location().symbolic.key.group() + } } pub(crate) struct FindingBuilder<'doc> { diff --git a/crates/zizmor/src/github_api.rs b/crates/zizmor/src/github_api.rs index 73dd5436..991bf617 100644 --- a/crates/zizmor/src/github_api.rs +++ b/crates/zizmor/src/github_api.rs @@ -3,10 +3,10 @@ //! Build on synchronous reqwest to avoid octocrab's need to taint //! the whole codebase with async. -use std::{io::Read, ops::Deref, path::Path}; +use std::{fmt::Display, io::Read, ops::Deref, str::FromStr}; use anyhow::{Context, Result, anyhow}; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use flate2::read::GzDecoder; use http_cache_reqwest::{ CACacheManager, Cache, CacheMode, CacheOptions, HttpCache, HttpCacheOptions, @@ -59,6 +59,29 @@ impl GitHubHost { } } +impl Default for GitHubHost { + fn default() -> Self { + Self::Standard("github.com".into()) + } +} + +impl Display for GitHubHost { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Enterprise(host) => write!(f, "{host}"), + Self::Standard(host) => write!(f, "{host}"), + } + } +} + +impl FromStr for GitHubHost { + type Err = String; + + fn from_str(s: &str) -> Result { + Self::new(s) + } +} + /// A sanitized GitHub access token. #[derive(Clone)] pub(crate) struct GitHubToken(String); @@ -80,14 +103,15 @@ impl GitHubToken { #[derive(Clone)] pub(crate) struct Client { api_base: String, + host: GitHubHost, http: ClientWithMiddleware, } impl Client { pub(crate) fn new( - hostname: &GitHubHost, - token: &GitHubToken, - cache_dir: &Path, + host: GitHubHost, + token: GitHubToken, + cache_dir: Utf8PathBuf, ) -> anyhow::Result { let mut headers = HeaderMap::new(); headers.insert(USER_AGENT, "zizmor".parse().unwrap()); @@ -127,11 +151,16 @@ impl Client { .build(); Ok(Self { - api_base: hostname.to_api_url(), + api_base: host.to_api_url(), + host, http, }) } + pub(crate) fn host(&self) -> &GitHubHost { + &self.host + } + async fn paginate( &self, endpoint: &str, @@ -339,6 +368,57 @@ impl Client { .map_err(Into::into) } + /// Fetch a single file from the given remote repository slug. + /// + /// Returns the file contents as a `String` if the file exists, + /// or `None` if the request produces a 404. + #[instrument(skip(self, slug, file))] + #[tokio::main] + pub(crate) async fn fetch_single_file( + &self, + slug: &RepoSlug, + file: &str, + ) -> Result> { + self.fetch_single_file_async(slug, file).await + } + + pub(crate) async fn fetch_single_file_async( + &self, + slug: &RepoSlug, + file: &str, + ) -> Result> { + tracing::debug!("fetching {file} from {slug}"); + + let url = format!( + "{api_base}/repos/{owner}/{repo}/contents/{file}", + api_base = self.api_base, + owner = slug.owner, + repo = slug.repo, + file = file + ); + + let resp = self + .http + .get(&url) + .header(ACCEPT, "application/vnd.github.raw+json") + .pipe(|req| match slug.git_ref.as_ref() { + Some(g) => req.query(&[("ref", g)]), + None => req, + }) + .send() + .await?; + + match resp.status() { + StatusCode::OK => { + let contents = resp.text().await?; + + Ok(Some(contents)) + } + StatusCode::NOT_FOUND => Ok(None), + _ => Err(resp.error_for_status().unwrap_err().into()), + } + } + /// Collect all workflows (and only workflows) defined in the given remote /// repository slug into the given input group. /// @@ -382,22 +462,15 @@ impl Client { .into_iter() .filter(|file| file.name.ends_with(".yml") || file.name.ends_with(".yaml")) { - let file_url = format!("{url}/{file}", file = file.name); - tracing::debug!("fetching {file_url}"); - - let contents = self - .http - .get(file_url) - .header(ACCEPT, "application/vnd.github.raw+json") - .pipe(|req| match git_ref.as_ref() { - Some(g) => req.query(&[("ref", g)]), - None => req, - }) - .send() - .await? - .error_for_status()? - .text() - .await?; + let Some(contents) = self.fetch_single_file_async(slug, &file.path).await? else { + // This can only happen if we have some kind of TOCTOU + // discrepancy with the listing call above, e.g. a file + // was deleted on a branch immediately after we listed it. + anyhow::bail!( + "couldn't fetch workflow file {file} from {slug}", + file = file.name + ); + }; let key = InputKey::remote(slug, file.path)?; // TODO: Make strictness configurable here? diff --git a/crates/zizmor/src/lsp.rs b/crates/zizmor/src/lsp.rs index a5b7f563..1ee255ea 100644 --- a/crates/zizmor/src/lsp.rs +++ b/crates/zizmor/src/lsp.rs @@ -11,6 +11,7 @@ use crate::finding::location::Point; use crate::finding::{Persona, Severity}; use crate::models::action::Action; use crate::models::workflow::Workflow; +use crate::registry::input::{InputGroup, InputRegistry}; use crate::registry::{FindingRegistry, input::InputKey}; use crate::{AuditRegistry, AuditState}; @@ -181,10 +182,17 @@ impl Backend { anyhow::bail!("asked to audit unexpected file: {path}"); }; - let config = Config::default(); - let mut registry = FindingRegistry::new(None, None, Persona::Regular, &config); - for (_, audit) in self.audit_registry.iter_audits() { - registry.extend(audit.audit(&input)?); + let mut group = InputGroup::new(Config::default()); + group.register_input(input)?; + let mut input_registry = InputRegistry::new(); + input_registry.groups.insert("lsp".into(), group); + + let mut registry = FindingRegistry::new(&input_registry, None, None, Persona::Regular); + + for (input_key, input) in input_registry.iter_inputs() { + for (_, audit) in self.audit_registry.iter_audits() { + registry.extend(audit.audit(input, input_registry.get_config(input_key.group()))?); + } } let diagnostics = registry @@ -256,16 +264,9 @@ pub(crate) async fn run() -> anyhow::Result<()> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let config = Config::default(); + let state = AuditState::default(); - let audit_state = AuditState { - config: &config, - no_online_audits: false, - gh_client: None, - gh_hostname: crate::GitHubHost::Standard("github.com".into()), - }; - - let audits = AuditRegistry::default_audits(&audit_state)?; + let audits = AuditRegistry::default_audits(&state)?; let (service, socket) = LspService::new(|client| Backend { audit_registry: audits, client, diff --git a/crates/zizmor/src/main.rs b/crates/zizmor/src/main.rs index a6d54251..b66361ae 100644 --- a/crates/zizmor/src/main.rs +++ b/crates/zizmor/src/main.rs @@ -8,10 +8,11 @@ use std::{ use annotate_snippets::{Level, Renderer}; use anstream::{eprintln, println, stream::IsTerminal}; use anyhow::{Context, Result, anyhow}; +use camino::Utf8PathBuf; use clap::{Args, CommandFactory, Parser, ValueEnum, builder::NonEmptyStringValueParser}; use clap_complete::Generator; use clap_verbosity_flag::InfoLevel; -use config::Config; +use etcetera::AppStrategy as _; use finding::{Confidence, Persona, Severity}; use github_api::{GitHubHost, GitHubToken}; use indicatif::ProgressStyle; @@ -24,6 +25,8 @@ use tracing::{Span, info_span, instrument}; use tracing_indicatif::{IndicatifLayer, span_ext::IndicatifSpanExt}; use tracing_subscriber::{EnvFilter, layer::SubscriberExt as _, util::SubscriberInitExt as _}; +use crate::{config::Config, github_api::Client}; + mod audit; mod config; mod finding; @@ -70,7 +73,7 @@ struct App { gh_token: Option, /// The GitHub Server Hostname. Defaults to github.com - #[arg(long, env = "GH_HOST", default_value = "github.com", value_parser = GitHubHost::new)] + #[arg(long, env = "GH_HOST", default_value_t)] gh_hostname: GitHubHost, /// Perform only offline audits. @@ -96,8 +99,9 @@ struct App { #[arg(long, value_enum, value_name = "MODE")] color: Option, - /// The configuration file to load. By default, any config will be - /// discovered relative to $CWD. + /// The configuration file to load. + /// This loads a single configuration file across all input groups, + /// which may not be what you intend. #[arg( short, long, @@ -125,8 +129,8 @@ struct App { /// The directory to use for HTTP caching. By default, a /// host-appropriate user-caching directory will be used. - #[arg(long)] - cache_dir: Option, + #[arg(long, default_value_t = App::default_cache_dir(), hide_default_value = true)] + cache_dir: Utf8PathBuf, /// Control which kinds of inputs are collected for auditing. /// @@ -176,6 +180,20 @@ struct App { inputs: Vec, } +impl App { + fn default_cache_dir() -> Utf8PathBuf { + etcetera::choose_app_strategy(etcetera::AppStrategyArgs { + top_level_domain: "io.github".into(), + author: "woodruffw".into(), + app_name: "zizmor".into(), + }) + .expect("failed to determine default cache directory") + .cache_dir() + .try_into() + .expect("failed to turn cache directory into a sane path") + } +} + #[cfg(feature = "lsp")] #[derive(Args)] #[group(multiple = true, conflicts_with = "inputs")] @@ -353,17 +371,25 @@ pub(crate) fn tips(err: impl AsRef, tips: &[impl AsRef]) -> String { format!("{}", renderer.render(message)) } +/// State used when collecting input groups. +pub(crate) struct CollectionOptions { + pub(crate) mode: CollectionMode, + pub(crate) strict: bool, + pub(crate) no_config: bool, + /// Global configuration, if any. + pub(crate) global_config: Option, +} + #[instrument(skip_all)] fn collect_inputs( inputs: Vec, - mode: CollectionMode, - strict: bool, - state: &AuditState, + options: &CollectionOptions, + gh_client: Option<&Client>, ) -> Result { let mut registry = InputRegistry::new(); for input in inputs.into_iter() { - registry.register_group(input, mode, strict, state)?; + registry.register_group(input, options, gh_client)?; } if registry.len() == 0 { @@ -477,23 +503,28 @@ fn run() -> Result { reg.with(indicatif_layer).init(); } - let config = Config::new(&app).map_err(|e| { - anyhow!(tips( - format!("failed to load config: {e:#}"), - &[ - "check your configuration file for errors", - "see: https://docs.zizmor.sh/configuration/" - ] - )) - })?; + let global_config = Config::global(&app)?; - let audit_state = AuditState::new(&app, &config)?; - let registry = collect_inputs(app.inputs, app.collect, app.strict_collection, &audit_state)?; + let gh_client = app + .gh_token + .map(|token| Client::new(app.gh_hostname, token, app.cache_dir)) + .transpose()?; - let audit_registry = AuditRegistry::default_audits(&audit_state)?; + let collection_options = CollectionOptions { + mode: app.collect, + strict: app.strict_collection, + no_config: app.no_config, + global_config, + }; + + let registry = collect_inputs(app.inputs, &collection_options, gh_client.as_ref())?; + + let state = AuditState::new(app.no_online_audits, gh_client); + + let audit_registry = AuditRegistry::default_audits(&state)?; let mut results = - FindingRegistry::new(app.min_severity, app.min_confidence, app.persona, &config); + FindingRegistry::new(®istry, app.min_severity, app.min_confidence, app.persona); { // Note: block here so that we drop the span here at the right time. let span = info_span!("audit"); @@ -504,15 +535,16 @@ fn run() -> Result { let _guard = span.enter(); - for (_, input) in registry.iter_inputs() { + for (input_key, input) in registry.iter_inputs() { Span::current().pb_set_message(input.key().filename()); + let config = registry.get_config(input_key.group()); for (name, audit) in audit_registry.iter_audits() { tracing::debug!( "running {name} on {input}", name = name, input = input.key() ); - results.extend(audit.audit(input).with_context(|| { + results.extend(audit.audit(input, config).with_context(|| { format!("{name} failed on {input}", input = input.key().filename()) })?); Span::current().pb_inc(1); diff --git a/crates/zizmor/src/models/uses.rs b/crates/zizmor/src/models/uses.rs index 91099ede..e70ec722 100644 --- a/crates/zizmor/src/models/uses.rs +++ b/crates/zizmor/src/models/uses.rs @@ -37,7 +37,7 @@ static REPOSITORY_USES_PATTERN: LazyLock = LazyLock::new(|| { /// Represents a pattern for matching repository `uses` references. /// These patterns are ordered by specificity; more specific patterns /// should be listed first. -#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub(crate) enum RepositoryUsesPattern { /// Matches exactly `owner/repo/subpath@ref`. ExactWithRef { diff --git a/crates/zizmor/src/registry.rs b/crates/zizmor/src/registry.rs index 25b331b1..d3638d2e 100644 --- a/crates/zizmor/src/registry.rs +++ b/crates/zizmor/src/registry.rs @@ -7,9 +7,8 @@ use indexmap::IndexMap; use crate::{ audit::{self, Audit, AuditLoadError}, - config::Config, finding::{Confidence, Finding, Persona, Severity}, - registry::input::InputKey, + registry::input::{InputKey, InputRegistry}, state::AuditState, tips, }; @@ -110,7 +109,7 @@ impl std::fmt::Debug for AuditRegistry { /// A registry of all findings discovered during a `zizmor` run. pub(crate) struct FindingRegistry<'a> { - config: &'a Config, + input_registry: &'a InputRegistry, minimum_severity: Option, minimum_confidence: Option, persona: Persona, @@ -122,13 +121,13 @@ pub(crate) struct FindingRegistry<'a> { impl<'a> FindingRegistry<'a> { pub(crate) fn new( + input_registry: &'a InputRegistry, minimum_severity: Option, minimum_confidence: Option, persona: Persona, - config: &'a Config, ) -> Self { Self { - config, + input_registry, minimum_severity, minimum_confidence, persona, @@ -154,7 +153,10 @@ impl<'a> FindingRegistry<'a> { || self .minimum_confidence .is_some_and(|min| min > finding.determinations.confidence) - || self.config.ignores(&finding) + || self + .input_registry + .get_config(finding.input_group()) + .ignores(&finding) { self.ignored.push(finding); } else { diff --git a/crates/zizmor/src/registry/input.rs b/crates/zizmor/src/registry/input.rs index f0224b78..7092222e 100644 --- a/crates/zizmor/src/registry/input.rs +++ b/crates/zizmor/src/registry/input.rs @@ -5,18 +5,18 @@ use std::{ str::FromStr as _, }; -use anyhow::Context as _; +use anyhow::Context; use camino::{Utf8Path, Utf8PathBuf}; use owo_colors::OwoColorize as _; use serde::Serialize; use thiserror::Error; use crate::{ - CollectionMode, + CollectionMode, CollectionOptions, audit::AuditInput, - github_api::GitHubHost, + config::Config, + github_api::{Client, GitHubHost}, models::{action::Action, workflow::Workflow}, - state::AuditState, tips, }; @@ -243,7 +243,7 @@ impl InputKey { } /// Returns the group this input belongs to. - fn group(&self) -> &Group { + pub(crate) fn group(&self) -> &Group { match self { InputKey::Local(local) => &local.group, InputKey::Remote(remote) => &remote.group, @@ -270,18 +270,20 @@ impl From<&RepoSlug> for Group { /// A group of inputs collected from the same source. pub(crate) struct InputGroup { /// The collected inputs. - pub(crate) inputs: BTreeMap, - // TODO: Config will go here. + inputs: BTreeMap, + /// The configuration for this group. + config: Config, } impl InputGroup { - fn new() -> Self { + pub(crate) fn new(config: Config) -> Self { Self { inputs: Default::default(), + config, } } - fn register_input(&mut self, input: AuditInput) -> anyhow::Result<()> { + pub(crate) fn register_input(&mut self, input: AuditInput) -> anyhow::Result<()> { if self.inputs.contains_key(input.key()) { return Err(anyhow::anyhow!( "can't register {key} more than once", @@ -324,7 +326,12 @@ impl InputGroup { } } - fn collect_from_file(&mut self, path: &Utf8Path, strict: bool) -> anyhow::Result<()> { + fn collect_from_file(path: &Utf8Path, options: &CollectionOptions) -> anyhow::Result { + let config = Config::discover(options, || Config::discover_local(path)) + .with_context(|| format!("failed to discover configuration for {path}"))?; + + let mut group = Self::new(config); + // When collecting individual files, we don't know which part // of the input path is the prefix. let (key, kind) = match (path.file_stem(), path.extension()) { @@ -340,15 +347,17 @@ impl InputGroup { }; let contents = std::fs::read_to_string(path)?; - self.register(kind, contents, key, strict) + group.register(kind, contents, key, options.strict)?; + + Ok(group) } - fn collect_from_dir( - &mut self, - path: &Utf8Path, - mode: CollectionMode, - strict: bool, - ) -> anyhow::Result<()> { + fn collect_from_dir(path: &Utf8Path, options: &CollectionOptions) -> anyhow::Result { + let config = Config::discover(options, || Config::discover_local(path)) + .with_context(|| format!("failed to discover configuration for directory {path}"))?; + + let mut group = Self::new(config); + // Start with all filters disabled, i.e. walk everything. let mut walker = ignore::WalkBuilder::new(path); let walker = walker.standard_filters(false); @@ -364,7 +373,7 @@ impl InputGroup { // zizmor integrators. // // See: https://github.com/zizmorcore/zizmor/issues/596 - if mode.respects_gitignore() { + if options.mode.respects_gitignore() { walker .require_git(false) .git_ignore(true) @@ -376,7 +385,7 @@ impl InputGroup { let entry = entry?; let entry = <&Utf8Path>::try_from(entry.path())?; - if mode.workflows() + if options.mode.workflows() && entry.is_file() && matches!(entry.extension(), Some("yml" | "yaml")) && entry @@ -385,29 +394,27 @@ impl InputGroup { { let key = InputKey::local(Group(path.as_str().into()), entry, Some(path))?; let contents = std::fs::read_to_string(entry)?; - self.register(InputKind::Workflow, contents, key, strict)?; + group.register(InputKind::Workflow, contents, key, options.strict)?; } - if mode.actions() + if options.mode.actions() && entry.is_file() && matches!(entry.file_name(), Some("action.yml" | "action.yaml")) { let key = InputKey::local(Group(path.as_str().into()), entry, Some(path))?; let contents = std::fs::read_to_string(entry)?; - self.register(InputKind::Action, contents, key, strict)?; + group.register(InputKind::Action, contents, key, options.strict)?; } } - Ok(()) + Ok(group) } fn collect_from_repo_slug( - &mut self, raw_slug: &str, - state: &AuditState, - mode: CollectionMode, - _strict: bool, - ) -> anyhow::Result<()> { + options: &CollectionOptions, + gh_client: Option<&Client>, + ) -> anyhow::Result { let Ok(slug) = RepoSlug::from_str(raw_slug) else { return Err(anyhow::anyhow!(tips( format!("invalid input: {raw_slug}"), @@ -420,7 +427,7 @@ impl InputGroup { ))); }; - let client = state.gh_client.as_ref().ok_or_else(|| { + let client = gh_client.ok_or_else(|| { anyhow::anyhow!(tips( format!( "can't retrieve repository: {raw_slug}", @@ -434,30 +441,36 @@ impl InputGroup { )) })?; - if matches!(mode, CollectionMode::WorkflowsOnly) { + let config = Config::discover(options, || Config::discover_remote(client, &slug)) + .with_context(|| format!("failed to discover configuration for {slug}"))?; + let mut group = Self::new(config); + + if matches!(options.mode, CollectionMode::WorkflowsOnly) { // Performance: if we're *only* collecting workflows, then we // can save ourselves a full repo download and only fetch the // repo's workflow files. - client.fetch_workflows(&slug, self)?; + client.fetch_workflows(&slug, &mut group)?; } else { - let before = self.len(); - let host = match &state.gh_hostname { + let before = group.len(); + let host = match client.host() { GitHubHost::Enterprise(address) => address.as_str(), GitHubHost::Standard(_) => "github.com", }; - client.fetch_audit_inputs(&slug, self).with_context(|| { - tips( - format!( - "couldn't collect inputs from https://{host}/{owner}/{repo}", - host = host, - owner = slug.owner, - repo = slug.repo - ), - &["confirm the repository exists and that you have access to it"], - ) - })?; - let after = self.len(); + client + .fetch_audit_inputs(&slug, &mut group) + .with_context(|| { + tips( + format!( + "couldn't collect inputs from https://{host}/{owner}/{repo}", + host = host, + owner = slug.owner, + repo = slug.repo + ), + &["confirm the repository exists and that you have access to it"], + ) + })?; + let after = group.len(); let len = after - before; tracing::info!( @@ -467,26 +480,22 @@ impl InputGroup { ); } - Ok(()) + Ok(group) } pub(crate) fn collect( request: &str, - mode: CollectionMode, - strict: bool, - state: &AuditState, + options: &CollectionOptions, + gh_client: Option<&Client>, ) -> anyhow::Result { let path = Utf8Path::new(request); - let mut group = Self::new(); if path.is_file() { - group.collect_from_file(path, strict)?; + Self::collect_from_file(path, options) } else if path.is_dir() { - group.collect_from_dir(path, mode, strict)?; + Self::collect_from_dir(path, options) } else { - group.collect_from_repo_slug(request, state, mode, strict)?; + Self::collect_from_repo_slug(request, options, gh_client) } - - Ok(group) } pub(crate) fn len(&self) -> usize { @@ -518,15 +527,14 @@ impl InputRegistry { pub(crate) fn register_group( &mut self, name: String, - mode: CollectionMode, - strict: bool, - state: &AuditState, + options: &CollectionOptions, + gh_client: Option<&Client>, ) -> anyhow::Result<()> { // If the group has already been registered, then the user probably // duplicated the input multiple times on the command line by accident. // We just ignore any duplicate registrations. if let btree_map::Entry::Vacant(e) = self.groups.entry(Group(name.clone())) { - e.insert(InputGroup::collect(&name, mode, strict, state)?); + e.insert(InputGroup::collect(&name, options, gh_client)?); } Ok(()) @@ -544,6 +552,15 @@ impl InputRegistry { .and_then(|group| group.inputs.get(key)) .expect("API misuse: requested an un-registered input") } + + /// Get a reference to the configuration for a given input group. + pub(crate) fn get_config(&self, group: &Group) -> &Config { + &self + .groups + .get(group) + .expect("API misuse: requested config for an un-registered input") + .config + } } #[cfg(test)] diff --git a/crates/zizmor/src/state.rs b/crates/zizmor/src/state.rs index 3401dbde..eaea362b 100644 --- a/crates/zizmor/src/state.rs +++ b/crates/zizmor/src/state.rs @@ -1,51 +1,28 @@ //! zizmor's runtime state, including application-level caching. -use std::path::PathBuf; +use crate::github_api::Client; -use etcetera::{AppStrategy, AppStrategyArgs, choose_app_strategy}; - -use crate::{ - App, - config::Config, - github_api::{Client, GitHubHost}, -}; - -#[derive(Clone)] -pub(crate) struct AuditState<'a> { - pub(crate) config: &'a Config, +pub(crate) struct AuditState { + /// Whether online audits should be skipped. pub(crate) no_online_audits: bool, /// A cache-configured GitHub API client, if a GitHub API token is given. pub(crate) gh_client: Option, - pub(crate) gh_hostname: GitHubHost, } -impl<'a> AuditState<'a> { - pub(crate) fn new(app: &App, config: &'a Config) -> anyhow::Result { - let cache_dir = match &app.cache_dir { - Some(cache_dir) => PathBuf::from(cache_dir), - None => choose_app_strategy(AppStrategyArgs { - top_level_domain: "io.github".into(), - author: "woodruffw".into(), - app_name: "zizmor".into(), - }) - // NOTE: no point in failing gracefully here. - .expect("failed to determine default cache directory") - .cache_dir(), - }; - - tracing::debug!("using cache directory: {cache_dir:?}"); - - let gh_client = app - .gh_token - .as_ref() - .map(|token| Client::new(&app.gh_hostname, token, &cache_dir)) - .transpose()?; - - Ok(Self { - config, - no_online_audits: app.no_online_audits, +impl AuditState { + pub(crate) fn new(no_online_audits: bool, gh_client: Option) -> Self { + Self { + no_online_audits, gh_client, - gh_hostname: app.gh_hostname.clone(), - }) + } + } +} + +impl Default for AuditState { + fn default() -> Self { + Self { + no_online_audits: true, + gh_client: None, + } } } diff --git a/crates/zizmor/tests/integration/common.rs b/crates/zizmor/tests/integration/common.rs index bc8c6e85..0aace0db 100644 --- a/crates/zizmor/tests/integration/common.rs +++ b/crates/zizmor/tests/integration/common.rs @@ -1,26 +1,33 @@ use anyhow::{Context as _, Result}; +use camino::Utf8PathBuf; use regex::{Captures, Regex}; -use std::{env::current_dir, io::ErrorKind}; +use std::{env::current_dir, io::ErrorKind, sync::LazyLock}; use assert_cmd::Command; -pub fn input_under_test(name: &str) -> String { +static TEST_PREFIX: LazyLock = LazyLock::new(|| { let current_dir = current_dir().expect("Cannot figure out current directory"); let file_path = current_dir .join("tests") .join("integration") - .join("test-data") - .join(name); + .join("test-data"); if !file_path.exists() { - panic!("Cannot find input under test: {}", file_path.display()); + panic!("Cannot find test data directory: {}", file_path.display()); } - file_path - .to_str() - .expect("Cannot create string reference for file path") - .to_string() + Utf8PathBuf::try_from(file_path).expect("Cannot create UTF-8 path from test data directory") +}); + +pub fn input_under_test(name: &str) -> String { + let file_path = TEST_PREFIX.join(name); + + if !file_path.exists() { + panic!("Cannot find input under test: {file_path}"); + } + + file_path.to_string() } pub enum OutputMode { @@ -36,6 +43,7 @@ pub struct Zizmor { offline: bool, inputs: Vec, config: Option, + no_config: bool, output: OutputMode, expects_failure: bool, } @@ -51,6 +59,7 @@ impl Zizmor { offline: true, inputs: vec![], config: None, + no_config: false, output: OutputMode::Stdout, expects_failure: false, } @@ -81,6 +90,11 @@ impl Zizmor { self } + pub fn no_config(mut self, flag: bool) -> Self { + self.no_config = flag; + self + } + pub fn unbuffer(mut self, flag: bool) -> Self { self.unbuffer = flag; self @@ -113,12 +127,18 @@ impl Zizmor { std::env::var("GH_TOKEN").context("online tests require GH_TOKEN to be set")?; } - if let Some(config) = self.config { - self.cmd.arg("--config").arg(config); - } else { + if self.no_config && self.config.is_some() { + anyhow::bail!("API misuse: cannot set both --no-config and --config"); + } + + if self.no_config { self.cmd.arg("--no-config"); } + if let Some(config) = &self.config { + self.cmd.arg("--config").arg(config); + } + for input in &self.inputs { self.cmd.arg(input); } @@ -170,6 +190,11 @@ impl Zizmor { } } + let config_placeholder = "@@CONFIG@@"; + if let Some(config) = &self.config { + raw = raw.replace(config, config_placeholder); + } + let input_placeholder = "@@INPUT@@"; for input in &self.inputs { raw = raw.replace(input, input_placeholder); @@ -185,6 +210,12 @@ impl Zizmor { .into_owned(); } + // Fallback: replace any lingering absolute paths. + // TODO: Maybe just use this everywhere instead of the special + // replacements above? + let test_prefix_placeholder = "@@TEST_PREFIX@@"; + raw = raw.replace(TEST_PREFIX.as_str(), test_prefix_placeholder); + Ok(raw) } } diff --git a/crates/zizmor/tests/integration/config.rs b/crates/zizmor/tests/integration/config.rs new file mode 100644 index 00000000..ca345760 --- /dev/null +++ b/crates/zizmor/tests/integration/config.rs @@ -0,0 +1,178 @@ +//! Configuration discovery tests. + +use crate::common::{OutputMode, input_under_test, zizmor}; + +/// Ensures we correctly discover a configuration file at the root +/// of a given input directory, i.e. `config-in-root/zizmor.yml` in +/// this case. +#[test] +fn test_discovers_config_in_root() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .input(input_under_test("config-scenarios/config-in-root")) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we correctly discover a configuration file in the root +/// directory from an input filename, i.e. going from +/// `config-in-root/.github/workflows/hackme.yml` +/// to `config-in-root/zizmor.yml` in this case. +#[test] +fn test_discovers_config_in_root_from_file_input() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .input(input_under_test( + "config-scenarios/config-in-root/.github/workflows/hackme.yml" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we correctly discover a configuration file in the root +/// directory from a child input directory, i.e. going from +/// `config-in-root/.github/workflows/` to `config-in-root/zizmor.yml` +/// in this case. +#[test] +fn test_discovers_config_in_root_from_child_dir() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .input(input_under_test( + "config-scenarios/config-in-root/.github/workflows" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we ignore a configuration file in the root of a given +/// input directory when `--no-config` is specified. +#[test] +fn test_ignores_config_in_root() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .no_config(true) + .input(input_under_test("config-scenarios/config-in-root")) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we ignore a configuration file in the root directory +/// from an input filename when `--no-config` is specified. +#[test] +fn test_ignores_config_in_root_from_file_input() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .no_config(true) + .input(input_under_test( + "config-scenarios/config-in-root/.github/workflows/hackme.yml" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we ignore a configuration file in the root directory +/// from a child input directory when `--no-config` is specified. +#[test] +fn test_ignores_config_in_root_from_child_dir() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .no_config(true) + .input(input_under_test( + "config-scenarios/config-in-root/.github/workflows" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we correctly discover a configuration file in a `.github` +/// subdirectory of a given input directory, i.e. +/// `config-in-dotgithub/.github/zizmor.yml` in this case. +#[test] +fn test_discovers_config_in_dotgithub() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .input(input_under_test("config-scenarios/config-in-dotgithub")) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we correctly discover a configuration file in a `.github` +/// subdirectory from an input filename, i.e. going from +/// `config-in-dotgithub/.github/workflows/hackme.yml` +/// to `config-in-dotgithub/.github/zizmor.yml` in this case. +#[test] +fn test_discovers_config_in_dotgithub_from_file_input() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .input(input_under_test( + "config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we ignore a configuration file in a `.github` subdirectory +/// of a given input directory when `--no-config` is specified. +#[test] +fn test_ignores_config_in_dotgithub() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .no_config(true) + .input(input_under_test("config-scenarios/config-in-dotgithub")) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} + +/// Ensures we ignore a configuration file in a `.github` subdirectory +/// from an input filename when `--no-config` is specified. +#[test] +fn test_ignores_config_in_dotgithub_from_file_input() -> anyhow::Result<()> { + insta::assert_snapshot!( + zizmor() + .no_config(true) + .input(input_under_test( + "config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml" + )) + .setenv("RUST_LOG", "zizmor::config=debug") + .output(OutputMode::Both) + .run()? + ); + + Ok(()) +} diff --git a/crates/zizmor/tests/integration/main.rs b/crates/zizmor/tests/integration/main.rs index db78bf66..ac6ca89f 100644 --- a/crates/zizmor/tests/integration/main.rs +++ b/crates/zizmor/tests/integration/main.rs @@ -4,6 +4,8 @@ mod acceptance; /// Helpers. mod common; +/// Configuration discovery tests. +mod config; /// "Big picture" end-to-end tests, i.e. tests that typically exercise /// more than one audit or complex CLI functionality. mod e2e; diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub.snap b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub.snap new file mode 100644 index 00000000..7b64ea46 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub.snap @@ -0,0 +1,9 @@ +--- +source: crates/zizmor/tests/integration/config.rs +expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-dotgithub\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?" +snapshot_kind: text +--- +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, 1 suppressed) diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub_from_file_input.snap b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub_from_file_input.snap new file mode 100644 index 00000000..271a8494 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_dotgithub_from_file_input.snap @@ -0,0 +1,9 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +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, 1 suppressed) diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root.snap b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root.snap new file mode 100644 index 00000000..2fda5513 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root.snap @@ -0,0 +1,9 @@ +--- +source: crates/zizmor/tests/integration/config.rs +expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-root\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?" +snapshot_kind: text +--- +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, 1 suppressed) diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_child_dir.snap b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_child_dir.snap new file mode 100644 index 00000000..cf821a32 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_child_dir.snap @@ -0,0 +1,9 @@ +--- +source: crates/zizmor/tests/integration/config.rs +expression: "zizmor().input(input_under_test(\"config-scenarios/config-in-root/.github/workflows\")).setenv(\"RUST_LOG\",\n\"zizmor::config=debug\").output(OutputMode::Both).run()?" +snapshot_kind: text +--- +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, 1 suppressed) diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_file_input.snap b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_file_input.snap new file mode 100644 index 00000000..4574fbe6 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__discovers_config_in_root_from_file_input.snap @@ -0,0 +1,9 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +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, 1 suppressed) diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub.snap b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub.snap new file mode 100644 index 00000000..989ba47e --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub.snap @@ -0,0 +1,20 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +DEBUG zizmor::config: skipping config discovery: explicitly disabled +error[template-injection]: code injection via template expansion + --> @@INPUT@@/.github/workflows/hackme.yml:16:40 + | +13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action accepts arbitrary code +14 | with: +15 | script: | + | ^^^^^^ via this input +16 | return "doing a thing: ${{ github.event.issue.title }}" + | ^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + +2 findings (1 suppressed): 0 unknown, 0 informational, 0 low, 0 medium, 1 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub_from_file_input.snap b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub_from_file_input.snap new file mode 100644 index 00000000..c13b3685 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_dotgithub_from_file_input.snap @@ -0,0 +1,20 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +DEBUG zizmor::config: skipping config discovery: explicitly disabled +error[template-injection]: code injection via template expansion + --> @@INPUT@@:16:40 + | +13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action accepts arbitrary code +14 | with: +15 | script: | + | ^^^^^^ via this input +16 | return "doing a thing: ${{ github.event.issue.title }}" + | ^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + +2 findings (1 suppressed): 0 unknown, 0 informational, 0 low, 0 medium, 1 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root.snap b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root.snap new file mode 100644 index 00000000..a0732ee7 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root.snap @@ -0,0 +1,20 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +DEBUG zizmor::config: skipping config discovery: explicitly disabled +error[template-injection]: code injection via template expansion + --> @@INPUT@@/.github/workflows/hackme.yml:16:40 + | +13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action accepts arbitrary code +14 | with: +15 | script: | + | ^^^^^^ via this input +16 | return "doing a thing: ${{ github.event.issue.title }}" + | ^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + +2 findings (1 suppressed): 0 unknown, 0 informational, 0 low, 0 medium, 1 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_child_dir.snap b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_child_dir.snap new file mode 100644 index 00000000..27f19682 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_child_dir.snap @@ -0,0 +1,20 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +DEBUG zizmor::config: skipping config discovery: explicitly disabled +error[template-injection]: code injection via template expansion + --> @@INPUT@@/hackme.yml:16:40 + | +13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action accepts arbitrary code +14 | with: +15 | script: | + | ^^^^^^ via this input +16 | return "doing a thing: ${{ github.event.issue.title }}" + | ^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + +2 findings (1 suppressed): 0 unknown, 0 informational, 0 low, 0 medium, 1 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_file_input.snap b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_file_input.snap new file mode 100644 index 00000000..07f389b2 --- /dev/null +++ b/crates/zizmor/tests/integration/snapshots/integration__config__ignores_config_in_root_from_file_input.snap @@ -0,0 +1,20 @@ +--- +source: crates/zizmor/tests/integration/config.rs +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()?" +snapshot_kind: text +--- +DEBUG zizmor::config: skipping config discovery: explicitly disabled +error[template-injection]: code injection via template expansion + --> @@INPUT@@:16:40 + | +13 | - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ action accepts arbitrary code +14 | with: +15 | script: | + | ^^^^^^ via this input +16 | return "doing a thing: ${{ github.event.issue.title }}" + | ^^^^^^^^^^^^^^^^^^^^^^^^ may expand into attacker-controllable code + | + = note: audit confidence → High + +2 findings (1 suppressed): 0 unknown, 0 informational, 0 low, 0 medium, 1 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap index 8e51789b..46fadbab 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap @@ -7,7 +7,6 @@ snapshot_kind: text INFO zizmor::registry: skipping impostor-commit: offline audits only requested INFO zizmor::registry: skipping ref-confusion: offline audits only requested INFO zizmor::registry: skipping known-vulnerable-actions: offline audits only requested - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: offline audits only requested INFO audit: zizmor: 🌈 completed .github/workflows/artipacked.yml INFO audit: zizmor: 🌈 completed .github/workflows/bot-conditions.yml diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_config_file.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_config_file.snap index daa2f9f2..87f93eac 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_config_file.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_config_file.snap @@ -1,9 +1,12 @@ --- -source: tests/integration/e2e.rs +source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).config(if cfg!(windows) { \"NUL\" } else\n{ \"/dev/null\" }).input(input_under_test(\"e2e-menagerie\")).run()?" snapshot_kind: text --- fatal: no audit was performed -error: failed to load config: missing field `rules` - = note: check your configuration file for errors - = note: see: https://docs.zizmor.sh/configuration/ +failed to load config file at `@@CONFIG@@` + +Caused by: + error: failed to load config: missing field `rules` + = note: check your configuration file for errors + = note: see: https://docs.zizmor.sh/configuration/ diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-10.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-10.snap index d1ed99ed..770f6947 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-10.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-10.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as action diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-2.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-2.snap index bd8fa0a2..aab39724 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-3.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-3.snap index a30b6842..889e9426 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-4.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-4.snap index 5b82e4e3..f27b0f24 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-5.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-5.snap index 4af48921..91fcd5b7 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-6.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-6.snap index a30b6842..889e9426 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-6.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-6.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-7.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-7.snap index a30b6842..889e9426 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-7.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-7.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-8.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-8.snap index 9ba0fb58..9fe49a59 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-8.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-8.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as action diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-9.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-9.snap index ff6e11db..b96d406b 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-9.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs-9.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as action diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs.snap index 80b49a99..8d705e37 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__invalid_inputs.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().expects_failure(true).input(input_under_test(&format!(\"invalid/{workflow_tc}.yml\"))).args([\"--strict-collection\"]).run()?" +snapshot_kind: text --- fatal: no audit was performed failed to load file://@@INPUT@@ as workflow diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_569.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_569.snap index 8d906c39..f8e1fa91 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_569.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_569.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().offline(false).output(OutputMode::Both).args([\"--no-online-audits\",\n\"--collect=workflows-only\"]).input(\"python/cpython@f963239ff1f986742d4c6bab2ab7b73f5a4047f6\").run()?" +snapshot_kind: text --- INFO zizmor::registry: skipping impostor-commit: offline audits only requested INFO zizmor::registry: skipping ref-confusion: offline audits only requested INFO zizmor::registry: skipping known-vulnerable-actions: offline audits only requested - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: offline audits only requested INFO audit: zizmor: 🌈 completed .github/workflows/add-issue-header.yml INFO audit: zizmor: 🌈 completed .github/workflows/build.yml @@ -91,18 +91,6 @@ help[obfuscation]: obfuscated usage of GitHub Actions features | = note: audit confidence → High -error[dangerous-triggers]: use of fundamentally insecure workflow trigger - --> .github/workflows/documentation-links.yml:5:1 - | - 5 | / on: - 6 | | pull_request_target: -... | -10 | | - 'Doc/**' -11 | | - '.github/workflows/doc.yml' - | |_________________________________^ pull_request_target is almost always used insecurely - | - = note: audit confidence → Medium - error[unpinned-uses]: unpinned action reference --> .github/workflows/documentation-links.yml:25:9 | @@ -183,4 +171,4 @@ error[unpinned-uses]: unpinned action reference | = note: audit confidence → High -78 findings (59 suppressed): 0 unknown, 0 informational, 1 low, 0 medium, 18 high +78 findings (1 ignored, 59 suppressed): 0 unknown, 0 informational, 1 low, 0 medium, 17 high diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_726.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_726.snap index 62c7c01b..be87a928 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_726.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__issue_726.snap @@ -7,7 +7,6 @@ snapshot_kind: text INFO zizmor::registry: skipping impostor-commit: offline audits only requested INFO zizmor::registry: skipping ref-confusion: offline audits only requested INFO zizmor::registry: skipping known-vulnerable-actions: offline audits only requested - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: offline audits only requested INFO audit: zizmor: 🌈 completed .github/actions/custom-action/action.yml INFO audit: zizmor: 🌈 completed .github/workflows/actions/custom-action/action.yml diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie-2.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie-2.snap index 715dd56d..0279aadb 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie-2.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().output(OutputMode::Both).args([\"--collect=all\"]).input(input_under_test(\"e2e-menagerie\")).run()?" +snapshot_kind: text --- INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: can't run without a GitHub API token INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/dummy-action-2/action.yml INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/workflows/another-dummy.yml diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie.snap index c226f078..adb777d1 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__menagerie.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().output(OutputMode::Both).input(input_under_test(\"e2e-menagerie\")).run()?" +snapshot_kind: text --- INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: can't run without a GitHub API token INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/dummy-action-2/action.yml INFO audit: zizmor: 🌈 completed @@INPUT@@/.github/workflows/another-dummy.yml diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__pr_960_backstop.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__pr_960_backstop.snap index 6e68f857..b50dab7f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__e2e__pr_960_backstop.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__pr_960_backstop.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/e2e.rs expression: "zizmor().output(OutputMode::Both).input(input_under_test(\"pr-960-backstop\")).run()?" +snapshot_kind: text --- INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token - INFO zizmor::registry: skipping forbidden-uses: audit not configured INFO zizmor::registry: skipping stale-action-refs: can't run without a GitHub API token INFO audit: zizmor: 🌈 completed @@INPUT@@/action.yml No findings to report. Good job! diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-2.snap index 0cd268d6..d0ff2362 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"artipacked.yml\")).run()?" +snapshot_kind: text --- warning[artipacked]: credential persistence through GitHub Actions artifacts --> @@INPUT@@:18:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-3.snap index 183607f1..f0ba592d 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"artipacked.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- warning[artipacked]: credential persistence through GitHub Actions artifacts --> @@INPUT@@:18:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-4.snap index 17ae2c1b..1f1b4d93 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"artipacked/issue-447-repro.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- warning[artipacked]: credential persistence through GitHub Actions artifacts --> @@INPUT@@:20:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-5.snap index 8573bace..bd5c2803 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"artipacked/demo-action/action.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- warning[artipacked]: credential persistence through GitHub Actions artifacts --> @@INPUT@@:9:7 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked.snap index 1978bd9e..6dc7027f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__artipacked.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"artipacked.yml\")).args([\"--persona=pedantic\"]).run()?" +snapshot_kind: text --- warning[artipacked]: credential persistence through GitHub Actions artifacts --> @@INPUT@@:18:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__bot_conditions.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__bot_conditions.snap index 26888178..8a80efa9 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__bot_conditions.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__bot_conditions.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"bot-conditions.yml\")).run()?" +snapshot_kind: text --- error[dangerous-triggers]: use of fundamentally insecure workflow trigger --> @@INPUT@@:1:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-10.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-10.snap index 0574d85b..7a1703f7 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-10.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-10.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/publisher-step.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:28:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-11.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-11.snap index bbc7b5a5..437150fa 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-11.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-11.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/issue-343-repro.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:5:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-15.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-15.snap index 32e9c044..00c6cd1e 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-15.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-15.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/issue-642-repro.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:21:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-3.snap index 237213f2..581a2c31 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/caching-opt-in-boolean-toggle.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:1:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-5.snap index 022ab2b8..05a14044 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/caching-opt-in-multi-value-toggle.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:1:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-9.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-9.snap index 8c3d8870..690e647f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-9.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__cache_poisoning-9.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"cache-poisoning/caching-opt-in-boolish-toggle.yml\")).run()?" +snapshot_kind: text --- error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack --> @@INPUT@@:4:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-10.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-10.snap index cc5fd45f..a50b2137 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-10.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-10.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/issue-472-repro.yml\")).run()?" +snapshot_kind: text --- warning[excessive-permissions]: overly broad permissions --> @@INPUT@@:20:3 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-11.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-11.snap index ae0698cd..27f61bc6 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-11.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-11.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/reusable-workflow-call.yml\")).run()?" +snapshot_kind: text --- warning[excessive-permissions]: overly broad permissions --> @@INPUT@@:7:3 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-12.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-12.snap index c6ee2e64..a0307cee 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-12.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-12.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/reusable-workflow-other-triggers.yml\")).run()?" +snapshot_kind: text --- warning[excessive-permissions]: overly broad permissions --> @@INPUT@@:1:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-2.snap index e885d7e2..de219502 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/issue-336-repro.yml\")).args([\"--pedantic\"]).run()?" +snapshot_kind: text --- error[excessive-permissions]: overly broad permissions --> @@INPUT@@:6:3 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-4.snap index 0ffb473a..915c1693 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/workflow-read-all.yml\")).run()?" +snapshot_kind: text --- warning[excessive-permissions]: overly broad permissions --> @@INPUT@@:5:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-5.snap index 856af2ab..85848d30 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/workflow-write-all.yml\")).run()?" +snapshot_kind: text --- error[excessive-permissions]: overly broad permissions --> @@INPUT@@:5:1 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-7.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-7.snap index 7fc1ce91..6e67b0d1 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-7.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-7.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/jobs-broaden-permissions.yml\")).run()?" +snapshot_kind: text --- warning[excessive-permissions]: overly broad permissions --> @@INPUT@@:8:3 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-8.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-8.snap index 7320ee4c..3e578197 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-8.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__excessive_permissions-8.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"excessive-permissions/workflow-write-explicit.yml\")).run()?" +snapshot_kind: text --- error[excessive-permissions]: overly broad permissions --> @@INPUT@@:7:3 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-2.snap index 641dbc4e..e5c42c4f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().config(input_under_test(&format!(\"forbidden-uses/configs/{config}.yml\"))).input(input_under_test(\"forbidden-uses/forbidden-uses-menagerie.yml\")).run()?" +snapshot_kind: text --- error[forbidden-uses]: forbidden action used --> @@INPUT@@:13:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-3.snap index aafe47f4..57e0e897 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().config(input_under_test(&format!(\"forbidden-uses/configs/{config}.yml\"))).input(input_under_test(\"forbidden-uses/forbidden-uses-menagerie.yml\")).run()?" +snapshot_kind: text --- error[forbidden-uses]: forbidden action used --> @@INPUT@@:13:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-4.snap index 05778562..bde1cac2 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().config(input_under_test(&format!(\"forbidden-uses/configs/{config}.yml\"))).input(input_under_test(\"forbidden-uses/forbidden-uses-menagerie.yml\")).run()?" +snapshot_kind: text --- error[forbidden-uses]: forbidden action used --> @@INPUT@@:14:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-5.snap index 4d8c7f02..7cd1daee 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().config(input_under_test(&format!(\"forbidden-uses/configs/{config}.yml\"))).input(input_under_test(\"forbidden-uses/forbidden-uses-menagerie.yml\")).run()?" +snapshot_kind: text --- error[forbidden-uses]: forbidden action used --> @@INPUT@@:13:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-6.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-6.snap index e050c321..59f4689a 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-6.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__forbidden_uses-6.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().config(input_under_test(&format!(\"forbidden-uses/configs/{config}.yml\"))).input(input_under_test(\"forbidden-uses/forbidden-uses-menagerie.yml\")).run()?" +snapshot_kind: text --- error[forbidden-uses]: forbidden action used --> @@INPUT@@:15:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-2.snap index e6bd21d7..c8f5162d 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"github-env/github-path.yml\")).run()?" +snapshot_kind: text --- error[github-env]: dangerous use of environment file --> @@INPUT@@:17:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-3.snap index e546b139..fed25abc 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"github-env/issue-397-repro.yml\")).run()?" +snapshot_kind: text --- error[github-env]: dangerous use of environment file --> @@INPUT@@:17:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env.snap index a5ea23f5..a81b6d73 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_env.snap @@ -1,6 +1,7 @@ --- -source: tests/integration/snapshot.rs +source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"github-env/action.yml\")).run()?" +snapshot_kind: text --- error[github-env]: dangerous use of environment file --> @@INPUT@@:10:7 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_output.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_output.snap index d09b503a..856406c6 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_output.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__github_output.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().offline(true).input(input_under_test(\"several-vulnerabilities.yml\")).args([\"--persona=auditor\",\n\"--format=github\"]).run()?" +snapshot_kind: text --- ::error file=@@INPUT@@,line=5,title=excessive-permissions::several-vulnerabilities.yml:5: overly broad permissions: uses write-all permissions ::error file=@@INPUT@@,line=11,title=excessive-permissions::several-vulnerabilities.yml:11: overly broad permissions: uses write-all permissions diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-2.snap index 4b6e2c67..a32824ac 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"insecure-commands.yml\")).run()?" +snapshot_kind: text --- error[insecure-commands]: execution of insecure workflow commands is enabled --> @@INPUT@@:11:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-3.snap index a61a2b9f..619f09ea 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"insecure-commands/action.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- error[insecure-commands]: execution of insecure workflow commands is enabled --> @@INPUT@@:18:7 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-4.snap index 10d53c51..5ea45c99 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"insecure-commands/issue-839-repro.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- error[insecure-commands]: execution of insecure workflow commands is enabled --> @@INPUT@@:11:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands.snap index fb6b5d84..e6605328 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__insecure_commands.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"insecure-commands.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- error[insecure-commands]: execution of insecure workflow commands is enabled --> @@INPUT@@:11:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation-2.snap index b49d3d3a..fca8e332 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"obfuscation/computed-indices.yml\")).args([\"--persona=pedantic\"]).run()?" +snapshot_kind: text --- help[obfuscation]: obfuscated usage of GitHub Actions features --> @@INPUT@@:14:23 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation.snap index b67234a7..480a90da 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__obfuscation.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"obfuscation.yml\")).run()?" +snapshot_kind: text --- help[obfuscation]: obfuscated usage of GitHub Actions features --> @@INPUT@@:13:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__overprovisioned_secrets.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__overprovisioned_secrets.snap index 92b39081..63aa4dab 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__overprovisioned_secrets.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__overprovisioned_secrets.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"overprovisioned-secrets.yml\")).run()?" +snapshot_kind: text --- warning[overprovisioned-secrets]: excessively provisioned secrets --> @@INPUT@@:15:18 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__secrets_inherit.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__secrets_inherit.snap index 8f271378..a3cde4b9 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__secrets_inherit.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__secrets_inherit.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"secrets-inherit.yml\")).run()?" +snapshot_kind: text --- warning[secrets-inherit]: secrets unconditionally inherited by called workflow --> @@INPUT@@:10:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-3.snap index 20385c2a..f2c8ffb5 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-3.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"self-hosted/self-hosted-runner-label.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[self-hosted-runner]: runs on a self-hosted runner --> @@INPUT@@:11:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-4.snap index 183f2c81..3fa6ff7d 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"self-hosted/self-hosted-runner-group.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[self-hosted-runner]: runs on a self-hosted runner --> @@INPUT@@:11:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-5.snap index 0690db42..e4bac382 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"self-hosted/self-hosted-matrix-dimension.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[self-hosted-runner]: runs on a self-hosted runner --> @@INPUT@@:13:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-6.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-6.snap index d54e86ee..bb6cdb42 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-6.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted-6.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"self-hosted/self-hosted-matrix-inclusion.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[self-hosted-runner]: runs on a self-hosted runner --> @@INPUT@@:13:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted.snap index 4757b8b4..7501cbed 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__self_hosted.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"self-hosted.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[self-hosted-runner]: runs on a self-hosted runner --> @@INPUT@@:13:5 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-11.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-11.snap index dc2a871a..0efef8e8 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-11.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-11.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/codeql-sinks.yml\")).run()?" +snapshot_kind: text --- error[template-injection]: code injection via template expansion --> @@INPUT@@:17:20 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-12.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-12.snap index 9e17b1f0..3054760f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-12.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-12.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/pwsh-script.yml\")).run()?" +snapshot_kind: text --- error[template-injection]: code injection via template expansion --> @@INPUT@@:16:61 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-13.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-13.snap index 76f47302..924b89a7 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-13.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-13.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/issue-883-repro/action.yml\")).run()?" +snapshot_kind: text --- note[template-injection]: code injection via template expansion --> @@INPUT@@:48:53 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-14.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-14.snap index b389380b..0aff637d 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-14.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-14.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/multiline-expression.yml\")).args([\"--persona=pedantic\"]).run()?" +snapshot_kind: text --- note[template-injection]: code injection via template expansion --> @@INPUT@@:14:13 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-2.snap index 77b8ec1e..f6538ad8 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/template-injection-dynamic-matrix.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[template-injection]: code injection via template expansion --> @@INPUT@@:22:36 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-4.snap index 311b8335..de9779e8 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-4.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-4.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/pr-317-repro.yml\")).run()?" +snapshot_kind: text --- warning[template-injection]: code injection via template expansion --> @@INPUT@@:28:20 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-5.snap index 51d04dd9..518141c7 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-5.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-5.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/static-env.yml\")).run()?" +snapshot_kind: text --- help[template-injection]: code injection via template expansion --> @@INPUT@@:43:20 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-6.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-6.snap index 2efbbd21..d60b82bb 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-6.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection-6.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/issue-339-repro.yml\")).run()?" +snapshot_kind: text --- info[template-injection]: code injection via template expansion --> @@INPUT@@:30:28 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection.snap index 74b1da44..c79892dc 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__template_injection.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"template-injection/template-injection-static-matrix.yml\")).args([\"--persona=auditor\"]).run()?" +snapshot_kind: text --- note[template-injection]: code injection via template expansion --> @@INPUT@@:21:36 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_images.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_images.snap index 19bccfea..9551ff10 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_images.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_images.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"unpinned-images.yml\")).args([\"--persona=pedantic\"]).run()?" +snapshot_kind: text --- error[unpinned-images]: unpinned image references --> @@INPUT@@:19:7 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-10.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-10.snap index b483a5bb..0e4bb20d 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-10.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-10.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: invalid pattern: foo/b*r - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: invalid pattern: foo/b*r diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-11.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-11.snap index dc5731b3..a046f50f 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-11.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-11.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: unknown variant `does not exist`, expected one of `any`, `ref-pin`, `hash-pin` - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: unknown variant `does not exist`, expected one of `any`, `ref-pin`, `hash-pin` diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-12.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-12.snap index 7c3a220a..1e3a40ab 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-12.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-12.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: can't use exact ref patterns here - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: can't use exact ref patterns here diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-3.snap index 098ed57e..de36527a 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-3.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-3.snap @@ -1,6 +1,7 @@ --- -source: tests/integration/snapshot.rs +source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"unpinned-uses/action.yml\")).args([\"--pedantic\"]).run()?" +snapshot_kind: text --- error[unpinned-uses]: unpinned action reference --> @@INPUT@@:8:7 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-6.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-6.snap index a0689b8b..7e2a0d13 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-6.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-6.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: invalid type: sequence, expected a map - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: invalid type: sequence, expected a map diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-7.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-7.snap index 5a48edd5..51c3635a 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-7.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-7.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: invalid pattern: lol - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: invalid pattern: lol diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-8.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-8.snap index 3de808b4..524913df 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-8.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-8.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: invalid pattern: foo/ - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: invalid pattern: foo/ diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-9.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-9.snap index 3f68636a..1a9a3fbc 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-9.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unpinned_uses-9.snap @@ -1,11 +1,11 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().expects_failure(true).config(input_under_test(&format!(\"unpinned-uses/configs/{tc}.yml\",))).input(input_under_test(\"unpinned-uses/menagerie-of-uses.yml\")).run()?" +snapshot_kind: text --- - INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token - INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token - INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token fatal: no audit was performed -error: failed to load audit: unpinned-uses - = note: invalid configuration: invalid pattern: */foo - = note: see: https://docs.zizmor.sh/audits/#unpinned-uses +failed to load config file at `@@CONFIG@@` + +Caused by: + 0: invalid unpinned-uses configuration + 1: invalid pattern: */foo diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unredacted_secrets.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unredacted_secrets.snap index dd0eceb2..576e2009 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unredacted_secrets.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unredacted_secrets.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"unredacted-secrets.yml\")).run()?" +snapshot_kind: text --- warning[unredacted-secrets]: leaked secret values --> @@INPUT@@:17:18 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unsound_contains.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unsound_contains.snap index e74854f4..ae4d9816 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__unsound_contains.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__unsound_contains.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"unsound-contains.yml\")).run()?" +snapshot_kind: text --- error[unsound-contains]: unsound contains condition --> @@INPUT@@:20:9 diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-2.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-2.snap index f9aaee56..bb10f9e3 100644 --- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-2.snap +++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-2.snap @@ -1,6 +1,7 @@ --- source: crates/zizmor/tests/integration/snapshot.rs expression: "zizmor().input(input_under_test(\"use-trusted-publishing/demo-action/action.yml\")).run()?" +snapshot_kind: text --- info[use-trusted-publishing]: prefer trusted publishing for authentication --> @@INPUT@@:9:7 diff --git a/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml new file mode 100644 index 00000000..31714168 --- /dev/null +++ b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/workflows/hackme.yml @@ -0,0 +1,16 @@ +name: hackme +on: + issues: + +permissions: {} + +jobs: + inject-me: + name: inject-me + runs-on: ubuntu-latest + + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + with: + script: | + return "doing a thing: ${{ github.event.issue.title }}" diff --git a/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/zizmor.yml b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/zizmor.yml new file mode 100644 index 00000000..9e7be950 --- /dev/null +++ b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-dotgithub/.github/zizmor.yml @@ -0,0 +1,4 @@ +rules: + template-injection: + ignore: + - hackme.yml diff --git a/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/.github/workflows/hackme.yml b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/.github/workflows/hackme.yml new file mode 100644 index 00000000..31714168 --- /dev/null +++ b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/.github/workflows/hackme.yml @@ -0,0 +1,16 @@ +name: hackme +on: + issues: + +permissions: {} + +jobs: + inject-me: + name: inject-me + runs-on: ubuntu-latest + + steps: + - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # tag=v7.0.1 + with: + script: | + return "doing a thing: ${{ github.event.issue.title }}" diff --git a/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/zizmor.yml b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/zizmor.yml new file mode 100644 index 00000000..9e7be950 --- /dev/null +++ b/crates/zizmor/tests/integration/test-data/config-scenarios/config-in-root/zizmor.yml @@ -0,0 +1,4 @@ +rules: + template-injection: + ignore: + - hackme.yml diff --git a/docs/configuration.md b/docs/configuration.md index 3d2ace0b..dd9a7fd2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,31 +13,52 @@ typically named `zizmor.yml`. [YAML]: https://learnxinyminutes.com/docs/yaml/ -## Precedence +## Discovery !!! note Configuration is *always* optional, and can always be disabled with `--no-config`. If `--no-config` is passed, no configuration is ever loaded. -`zizmor` will discover and load -configuration files in the following order of precedence: +!!! tip -1. Passed explicitly via `--config` on the command line, e.g. `--config - my-config.yml`. When passed explicitly, the file does *not* need to be - named `zizmor.yml`. -1. Passed explicitly via `ZIZMOR_CONFIG` in the environment, e.g. - `ZIZMOR_CONFIG=my-config.yml`. When passed explicitly, the file does - *not* need to be named `zizmor.yml`. -1. `${CWD}/.github/zizmor.yml` -1. `${CWD}/zizmor.yml` + `zizmor`'s configuration discovery behavior changed significantly + in `v1.13.0`. See the [release notes](./release-notes.md) for details. -For the last two discovery methods, `${CWD}` is the current working directory, -i.e. the directory that `zizmor` was executed from. +`zizmor` discovers configuration files in two conceptually distinct ways: -Only one configuration file is ever loaded. In other words: if both -`${CWD}/.github/zizmor.yml` and `${CWD}/zizmor.yml` exist, only the former -will be loaded, per the precedence rules above. +1. **Global** discovery: when explicitly given a configuration file via + `--config` or `ZIZMOR_CONFIG`, that file is used for **all** inputs. + + In other words: when a global configuration file is used no other + configuration files are discovered or loaded, even if they're present + according to the local discovery rules below. + +2. **Local** discovery: when no global configuration file is given, `zizmor` + looks for configuration files *for each given input*. The rules for this + discovery are as follows: + + * File inputs (e.g. `zizmor path/to/workflow.yml`): `zizmor` performs + directory discovery starting in the directory containing the given file. + + * Directory inputs (e.g. `zizmor .`): `zizmor` looks for a `zizmor.yml` or + `.github/zizmor.yml` in the given directory or any parent, up to the + filesystem root or the first `.git` directory. + + !!! note + + `zizmor .github/workflows/` is a special case: in this case, + discovery starts in `.github/`, the parent of the given directory. + + This is done to avoid confusion between a `zizmor.yml` config + file and a `zizmor.yml` workflow file. + + * Remote repository inputs (e.g. `zizmor owner/repo`): `zizmor` looks for + a `zizmor.yml` or `.github/zizmor.yml` in the root of the repository. + +In general, **most users will want to use local discovery**, which is the +default behavior. Global discovery only takes precedence when explicitly +requested with `--config` or `ZIZMOR_CONFIG`. ## Settings diff --git a/docs/release-notes.md b/docs/release-notes.md index ba21a269..880dab9e 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,38 @@ of `zizmor`. ## Next (UNRELEASED) +### Enhancements 🌱 + +* `zizmor`'s configuration discovery behavior has been significantly refactored, + making it easier to audit multiple independent inputs with their own + configuration files (#1094) + + For most users, this change should cause no compatibility issues. + For example, the following commands will continue to load the same + configuration files as before: + + ```sh + zizmor . + zizmor .github/ + ``` + + For other users, the behavior will change, but in a way that's intended + to correct a long-standing bug with configuration discovery. + In particular, the following commands will now behave differently: + + ```sh + # OLD: would discover config in $CWD + # NEW: will discover two different configs, one in each of the repos + zizmor ./repoA ./repoB + ``` + + Separately from these changes, `zizmor` continues to support + `--config ` and `ZIZMOR_CONFIG` with the exact same behavior as + before. + + See [Configuration - Discovery](./configuration.md#discovery) for a + detailed explanation of the new behavior. + ## 1.12.1 ### Bug Fixes 🐛 diff --git a/docs/snippets/help.txt b/docs/snippets/help.txt index cd34224d..60079a89 100644 --- a/docs/snippets/help.txt +++ b/docs/snippets/help.txt @@ -31,7 +31,7 @@ Options: --color Control the use of color in output [possible values: auto, always, never] -c, --config - The configuration file to load. By default, any config will be discovered relative to $CWD [env: ZIZMOR_CONFIG=] + The configuration file to load. This loads a single configuration file across all input groups, which may not be what you intend [env: ZIZMOR_CONFIG=] --no-config Disable all configuration loading --no-exit-codes diff --git a/docs/usage.md b/docs/usage.md index 75eb4ed3..50766caa 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -685,6 +685,14 @@ the `--config` argument. With `--config`, the file can be named anything: zizmor --config my-zizmor-config.yml /dir/to/audit ``` +!!! important + + When using `--config`, only a single configuration file is used + (instead of potentially discovering multiple configuration files, + one per input source). As a result, using `--config` is + **generally not recommended** unless auditing a single input source + (file, directory, or remote repository). + !!! tip Starting with `v1.8.0`, you can use the `ZIZMOR_CONFIG` environment @@ -693,7 +701,7 @@ zizmor --config my-zizmor-config.yml /dir/to/audit `ZIZMOR_CONFIG=my-config.yml` is equivalent to `--config my-config.yml`. -[will discover it]: ./configuration.md#precedence +[will discover it]: ./configuration.md#discovery See [Configuration: `rules..ignore`](./configuration.md#rulesidignore) for more details on writing ignore rules.