mirror of
https://github.com/zizmorcore/zizmor.git
synced 2025-12-23 08:47:33 +00:00
feat: load separate configs for input groups (#1094)
Some checks failed
Benchmark baseline / Continuous Benchmarking with Bencher (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Test site build (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (manylinux) (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (musllinux) (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build macOS wheels (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build source distribution (push) Has been cancelled
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (push) Has been cancelled
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Has been cancelled
CI / All tests pass (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Release (push) Has been cancelled
Some checks failed
Benchmark baseline / Continuous Benchmarking with Bencher (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Test site build (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (manylinux) (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (musllinux) (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build macOS wheels (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build source distribution (push) Has been cancelled
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (push) Has been cancelled
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Has been cancelled
CI / All tests pass (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Release (push) Has been cancelled
This commit is contained in:
parent
05fd75f958
commit
d75933e72d
136 changed files with 1528 additions and 664 deletions
|
|
@ -19,13 +19,14 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for AnonymousDefinition {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError> {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_workflow<'doc>(
|
||||
&self,
|
||||
workflow: &'doc crate::models::workflow::Workflow,
|
||||
_config: &crate::config::Config,
|
||||
) -> anyhow::Result<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -162,13 +162,14 @@ impl Artipacked {
|
|||
}
|
||||
|
||||
impl Audit for Artipacked {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError> {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_action<'doc>(
|
||||
&self,
|
||||
action: &'doc crate::models::action::Action,
|
||||
_config: &crate::config::Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
fn audit_normal_job<'doc>(
|
||||
&self,
|
||||
job: &super::NormalJob<'doc>,
|
||||
_config: &crate::config::Config,
|
||||
) -> Result<Vec<Finding<'doc>>> {
|
||||
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)
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ const BOT_ACTOR_IDS: &[&str] = &[
|
|||
];
|
||||
|
||||
impl Audit for BotConditions {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<super::Finding<'doc>>> {
|
||||
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)
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_normal_job<'doc>(&self, job: &NormalJob<'doc>) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_normal_job<'doc>(
|
||||
&self,
|
||||
job: &NormalJob<'doc>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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)
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError> {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> Result<Vec<Finding<'doc>>> {
|
||||
fn audit_workflow<'doc>(
|
||||
&self,
|
||||
workflow: &'doc Workflow,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
if workflow.has_pull_request_target() {
|
||||
findings.push(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ audit_meta!(
|
|||
pub(crate) struct ExcessivePermissions;
|
||||
|
||||
impl Audit for ExcessivePermissions {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Vec<Finding<'doc>>> {
|
||||
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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step, config)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'a>>> {
|
||||
self.process_step(step)
|
||||
self.process_step(step, config)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", untagged)]
|
||||
enum ForbiddenUsesConfig {
|
||||
Allow { allow: Vec<RepositoryUsesPattern> },
|
||||
Deny { deny: Vec<RepositoryUsesPattern> },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
@ -344,7 +345,11 @@ impl Audit for GitHubEnv {
|
|||
})
|
||||
}
|
||||
|
||||
fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
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");
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for HardcodedContainerCredentials {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError> {
|
||||
fn new(state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
fn audit_workflow<'doc>(
|
||||
&self,
|
||||
workflow: &'doc Workflow,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'a>>> {
|
||||
let mut findings = vec![];
|
||||
let Some(Uses::Repository(uses)) = step.uses() else {
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_workflow<'doc>(
|
||||
&self,
|
||||
workflow: &'doc Workflow,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
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)
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
@ -273,19 +274,21 @@ impl Audit for KnownVulnerableActions {
|
|||
.map(|client| KnownVulnerableActions { client })
|
||||
}
|
||||
|
||||
fn audit_step<'doc>(&self, step: &Step<'doc>) -> Result<Vec<Finding<'doc>>> {
|
||||
fn audit_step<'doc>(&self, step: &Step<'doc>, _config: &Config) -> Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'doc>(&self, step: &CompositeStep<'doc>) -> Result<Vec<Finding<'doc>>> {
|
||||
fn audit_composite_step<'doc>(
|
||||
&self,
|
||||
step: &CompositeStep<'doc>,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
fn audit_step<'doc>(&self, _step: &Step<'doc>) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
_step: &Step<'doc>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn audit_normal_job<'doc>(&self, job: &NormalJob<'doc>) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_normal_job<'doc>(
|
||||
&self,
|
||||
job: &NormalJob<'doc>,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn audit_workflow<'doc>(&self, workflow: &'doc Workflow) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_workflow<'doc>(
|
||||
&self,
|
||||
workflow: &'doc Workflow,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
Ok(vec![])
|
||||
}
|
||||
|
||||
fn audit_action<'doc>(&self, action: &'doc Action) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_action<'doc>(
|
||||
&self,
|
||||
action: &'doc Action,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
fn audit_raw<'doc>(
|
||||
&self,
|
||||
_input: &'doc AuditInput,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
#[instrument(skip(self, config))]
|
||||
fn audit<'doc>(
|
||||
&self,
|
||||
input: &'doc AuditInput,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_raw<'doc>(&self, input: &'doc AuditInput) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_raw<'doc>(
|
||||
&self,
|
||||
input: &'doc AuditInput,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'a>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ impl Audit for OverprovisionedSecrets {
|
|||
fn audit_raw<'doc>(
|
||||
&self,
|
||||
input: &'doc AuditInput,
|
||||
_config: &crate::config::Config,
|
||||
) -> anyhow::Result<Vec<super::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ impl RefConfusion {
|
|||
}
|
||||
|
||||
impl Audit for RefConfusion {
|
||||
fn new(state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
@ -122,7 +123,11 @@ impl Audit for RefConfusion {
|
|||
Ok(findings)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(&self, step: &CompositeStep<'a>) -> Result<Vec<Finding<'a>>> {
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
_config: &crate::config::Config,
|
||||
) -> Result<Vec<Finding<'a>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
let Some(Uses::Repository(uses)) = step.uses() else {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for SecretsInherit {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<super::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for SelfHostedRunner {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut results = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
@ -74,11 +75,15 @@ impl Audit for StaleActionRefs {
|
|||
.map(|client| StaleActionRefs { client })
|
||||
}
|
||||
|
||||
fn audit_step<'w>(&self, step: &Step<'w>) -> Result<Vec<Finding<'w>>> {
|
||||
fn audit_step<'w>(&self, step: &Step<'w>, _config: &Config) -> Result<Vec<Finding<'w>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(&self, step: &CompositeStep<'a>) -> Result<Vec<Finding<'a>>> {
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'a>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_step<'doc>(&self, step: &Step<'doc>) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
_config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'a>>> {
|
||||
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)
|
||||
}};
|
||||
|
|
|
|||
|
|
@ -42,13 +42,14 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for UnpinnedImages {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError> {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn audit_normal_job<'doc>(
|
||||
&self,
|
||||
job: &super::NormalJob<'doc>,
|
||||
_config: &crate::config::Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
let mut image_refs_with_locations: Vec<(DockerUses, SymbolicLocation<'doc>)> = vec![];
|
||||
|
|
|
|||
|
|
@ -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<Vec<Finding<'doc>>> {
|
||||
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<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let config = state
|
||||
.config
|
||||
.rule_config::<UnpinnedUsesConfig>(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<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'doc>>> {
|
||||
self.process_step(step, config)
|
||||
}
|
||||
|
||||
fn audit_composite_step<'a>(
|
||||
&self,
|
||||
step: &CompositeStep<'a>,
|
||||
config: &Config,
|
||||
) -> anyhow::Result<Vec<Finding<'a>>> {
|
||||
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<RepositoryUsesPattern, UsesPolicy>,
|
||||
}
|
||||
|
||||
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<String, Vec<(RepositoryUsesPattern, UsesPolicy)>>,
|
||||
|
||||
/// 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<UnpinnedUsesConfig> for UnpinnedUsesPolicies {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(config: UnpinnedUsesConfig) -> Result<Self, Self::Error> {
|
||||
let mut policy_tree: HashMap<String, Vec<(RepositoryUsesPattern, UsesPolicy)>> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for UnredactedSecrets {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
let mut findings = vec![];
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ impl UnsoundCondition {
|
|||
}
|
||||
|
||||
impl Audit for UnsoundCondition {
|
||||
fn new(_state: &crate::state::AuditState<'_>) -> Result<Self, super::AuditLoadError>
|
||||
fn new(_state: &crate::state::AuditState) -> Result<Self, super::AuditLoadError>
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
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<Vec<crate::finding::Finding<'doc>>> {
|
||||
self.process_conditions(action, action.conditions())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ audit_meta!(
|
|||
);
|
||||
|
||||
impl Audit for UnsoundContains {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError>
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
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<Vec<super::Finding<'w>>> {
|
||||
let conditions = job.conditions().filter_map(|(cond, loc)| {
|
||||
if let If::Expr(expr) = cond {
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ impl UseTrustedPublishing {
|
|||
}
|
||||
|
||||
impl Audit for UseTrustedPublishing {
|
||||
fn new(_state: &AuditState<'_>) -> Result<Self, AuditLoadError> {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError> {
|
||||
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<Vec<super::Finding<'doc>>> {
|
||||
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<Vec<Finding<'doc>>> {
|
||||
self.process_step(step)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<serde_yaml::Mapping>,
|
||||
}
|
||||
|
||||
/// 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<String, AuditRuleConfig>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn new(app: &App) -> Result<Self> {
|
||||
if app.no_config {
|
||||
return Ok(Self::default());
|
||||
}
|
||||
impl RawConfig {
|
||||
fn load(contents: &str) -> Result<Self> {
|
||||
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<T>(&self, ident: &str) -> Result<Option<T>>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
Ok(self
|
||||
.rules
|
||||
.get(ident)
|
||||
.and_then(|rule_config| rule_config.config.as_ref())
|
||||
.map(|policy| serde_yaml::from_value::<T>(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<RepositoryUsesPattern> },
|
||||
Deny { deny: Vec<RepositoryUsesPattern> },
|
||||
}
|
||||
|
||||
/// 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<RepositoryUsesPattern, UsesPolicy>,
|
||||
}
|
||||
|
||||
/// 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<String, Vec<(RepositoryUsesPattern, UsesPolicy)>>,
|
||||
|
||||
/// 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<UnpinnedUsesConfig> for UnpinnedUsesPolicies {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(config: UnpinnedUsesConfig) -> Result<Self, Self::Error> {
|
||||
let mut policy_tree: HashMap<String, Vec<(RepositoryUsesPattern, UsesPolicy)>> =
|
||||
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<ForbiddenUsesConfig>,
|
||||
pub(crate) unpinned_uses_policies: UnpinnedUsesPolicies,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Loads a [`Config`] from the given contents.
|
||||
fn load(contents: &str) -> Result<Self> {
|
||||
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::<UnpinnedUsesConfig>(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 <file>` 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<F>(options: &CollectionOptions, discover_fn: F) -> Result<Self>
|
||||
where
|
||||
F: FnOnce() -> Result<Option<Self>>,
|
||||
{
|
||||
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<Option<Self>> {
|
||||
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<Option<Self>> {
|
||||
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<Option<Self>> {
|
||||
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<Option<Self>> {
|
||||
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<T>(&self, ident: &str) -> Result<Option<T>>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
Ok(self
|
||||
.rules
|
||||
.get(ident)
|
||||
.and_then(|rule_config| rule_config.config.as_ref())
|
||||
.map(|policy| serde_yaml::from_value::<T>(serde_yaml::Value::Mapping(policy.clone())))
|
||||
.transpose()?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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, Self::Err> {
|
||||
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<Self> {
|
||||
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<T: DeserializeOwned>(
|
||||
&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<Option<String>> {
|
||||
self.fetch_single_file_async(slug, file).await
|
||||
}
|
||||
|
||||
pub(crate) async fn fetch_single_file_async(
|
||||
&self,
|
||||
slug: &RepoSlug,
|
||||
file: &str,
|
||||
) -> Result<Option<String>> {
|
||||
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?
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<GitHubToken>,
|
||||
|
||||
/// 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<ColorMode>,
|
||||
|
||||
/// 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<String>,
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
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<str>, tips: &[impl AsRef<str>]) -> 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<Config>,
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn collect_inputs(
|
||||
inputs: Vec<String>,
|
||||
mode: CollectionMode,
|
||||
strict: bool,
|
||||
state: &AuditState,
|
||||
options: &CollectionOptions,
|
||||
gh_client: Option<&Client>,
|
||||
) -> Result<InputRegistry> {
|
||||
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<ExitCode> {
|
|||
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<ExitCode> {
|
|||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ static REPOSITORY_USES_PATTERN: LazyLock<Regex> = 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 {
|
||||
|
|
|
|||
|
|
@ -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<Severity>,
|
||||
minimum_confidence: Option<Confidence>,
|
||||
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<Severity>,
|
||||
minimum_confidence: Option<Confidence>,
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<InputKey, AuditInput>,
|
||||
// TODO: Config will go here.
|
||||
inputs: BTreeMap<InputKey, AuditInput>,
|
||||
/// 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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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<Self> {
|
||||
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)]
|
||||
|
|
|
|||
|
|
@ -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<Client>,
|
||||
pub(crate) gh_hostname: GitHubHost,
|
||||
}
|
||||
|
||||
impl<'a> AuditState<'a> {
|
||||
pub(crate) fn new(app: &App, config: &'a Config) -> anyhow::Result<Self> {
|
||||
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<Client>) -> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Utf8PathBuf> = 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<String>,
|
||||
config: Option<String>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
178
crates/zizmor/tests/integration/config.rs
Normal file
178
crates/zizmor/tests/integration/config.rs
Normal file
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue