use thiserror::Error;
use tracing::instrument;
use yamlpath::Document;
use crate::{
config::Config,
finding::{
Finding, FindingBuilder,
location::{Routable, SymbolicLocation},
},
models::{
AsDocument,
action::{Action, CompositeStep},
dependabot::Dependabot,
workflow::{Job, NormalJob, ReusableWorkflowCallJob, Step, Workflow},
},
registry::input::InputKey,
state::AuditState,
};
pub(crate) mod anonymous_definition;
pub(crate) mod archived_uses;
pub(crate) mod artipacked;
pub(crate) mod bot_conditions;
pub(crate) mod cache_poisoning;
pub(crate) mod concurrency_limits;
pub(crate) mod dangerous_triggers;
pub(crate) mod dependabot_cooldown;
pub(crate) mod dependabot_execution;
pub(crate) mod excessive_permissions;
pub(crate) mod forbidden_uses;
pub(crate) mod github_env;
pub(crate) mod hardcoded_container_credentials;
pub(crate) mod impostor_commit;
pub(crate) mod insecure_commands;
pub(crate) mod known_vulnerable_actions;
pub(crate) mod misfeature;
pub(crate) mod obfuscation;
pub(crate) mod overprovisioned_secrets;
pub(crate) mod ref_confusion;
pub(crate) mod ref_version_mismatch;
pub(crate) mod secrets_inherit;
pub(crate) mod secrets_outside_env;
pub(crate) mod self_hosted_runner;
pub(crate) mod stale_action_refs;
pub(crate) mod superfluous_actions;
pub(crate) mod template_injection;
pub(crate) mod undocumented_permissions;
pub(crate) mod unpinned_images;
pub(crate) mod unpinned_uses;
pub(crate) mod unredacted_secrets;
pub(crate) mod unsound_condition;
pub(crate) mod unsound_contains;
pub(crate) mod use_trusted_publishing;
#[derive(Debug)]
pub(crate) enum AuditInput {
Workflow(Workflow),
Action(Action),
Dependabot(Dependabot),
}
impl AuditInput {
pub(crate) fn key(&self) -> &InputKey {
match self {
AuditInput::Workflow(workflow) => &workflow.key,
AuditInput::Action(action) => &action.key,
AuditInput::Dependabot(dependabot) => &dependabot.key,
}
}
pub(crate) fn link(&self) -> Option<&str> {
match self {
AuditInput::Workflow(workflow) => workflow.link.as_deref(),
AuditInput::Action(action) => action.link.as_deref(),
AuditInput::Dependabot(dependabot) => dependabot.link.as_deref(),
}
}
pub(crate) fn location(&self) -> SymbolicLocation<'_> {
match self {
AuditInput::Workflow(workflow) => workflow.location(),
AuditInput::Action(action) => action.location(),
AuditInput::Dependabot(dependabot) => dependabot.location(),
}
}
}
impl<'a> AsDocument<'a, 'a> for AuditInput {
fn as_document(&'a self) -> &'a Document {
match self {
AuditInput::Workflow(workflow) => workflow.as_document(),
AuditInput::Action(action) => action.as_document(),
AuditInput::Dependabot(dependabot) => dependabot.as_document(),
}
}
}
impl<'a> Routable<'a, 'a> for AuditInput {
fn route(&'a self) -> yamlpath::Route<'a> {
match self {
AuditInput::Workflow(workflow) => workflow.location().route,
AuditInput::Action(action) => action.location().route,
AuditInput::Dependabot(dependabot) => dependabot.location().route,
}
}
}
impl From<Workflow> for AuditInput {
fn from(value: Workflow) -> Self {
Self::Workflow(value)
}
}
impl From<Action> for AuditInput {
fn from(value: Action) -> Self {
Self::Action(value)
}
}
impl From<Dependabot> for AuditInput {
fn from(value: Dependabot) -> Self {
Self::Dependabot(value)
}
}
pub(crate) trait AuditCore {
fn ident() -> &'static str
where
Self: Sized;
fn desc() -> &'static str
where
Self: Sized;
fn url() -> &'static str
where
Self: Sized;
fn finding<'doc>() -> FindingBuilder<'doc>
where
Self: Sized,
{
FindingBuilder::new(Self::ident(), Self::desc(), Self::url())
}
fn err(error: impl Into<anyhow::Error>) -> AuditError
where
Self: Sized,
{
AuditError {
ident: Self::ident(),
source: error.into(),
}
}
}
macro_rules! audit_meta {
($t:ty, $id:literal, $desc:expr_2021) => {
use crate::audit::AuditCore;
impl AuditCore for $t {
fn ident() -> &'static str {
$id
}
fn desc() -> &'static str
where
Self: Sized,
{
$desc
}
fn url() -> &'static str {
concat!("https://docs.zizmor.sh/audits/#", $id)
}
}
};
}
pub(crate) use audit_meta;
#[derive(Error, Debug)]
pub(crate) enum AuditLoadError {
#[error("{0}")]
Skip(anyhow::Error),
}
#[derive(Error, Debug)]
#[error("error in '{ident}' audit")]
pub(crate) struct AuditError {
ident: &'static str,
source: anyhow::Error,
}
impl AuditError {
pub(crate) fn new(ident: &'static str, error: impl Into<anyhow::Error>) -> Self {
Self {
ident,
source: error.into(),
}
}
pub(crate) fn ident(&self) -> &'static str {
self.ident
}
}
#[async_trait::async_trait]
pub(crate) trait Audit: AuditCore {
fn new(state: &AuditState) -> Result<Self, AuditLoadError>
where
Self: Sized;
async fn audit_step<'doc>(
&self,
_step: &Step<'doc>,
_config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
Ok(vec![])
}
async fn audit_normal_job<'doc>(
&self,
job: &NormalJob<'doc>,
config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
let mut results = vec![];
for step in job.steps() {
results.extend(self.audit_step(&step, config).await?);
}
Ok(results)
}
async fn audit_reusable_job<'doc>(
&self,
_job: &ReusableWorkflowCallJob<'doc>,
_config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
Ok(vec![])
}
async fn audit_workflow<'doc>(
&self,
workflow: &'doc Workflow,
config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
let mut results = vec![];
for job in workflow.jobs() {
match job {
Job::NormalJob(normal) => {
results.extend(self.audit_normal_job(&normal, config).await?);
}
Job::ReusableWorkflowCallJob(reusable) => {
results.extend(self.audit_reusable_job(&reusable, config).await?);
}
}
}
Ok(results)
}
async fn audit_composite_step<'doc>(
&self,
_step: &CompositeStep<'doc>,
_config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
Ok(vec![])
}
async fn audit_action<'doc>(
&self,
action: &'doc Action,
config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
let mut results = vec![];
if let Some(steps) = action.steps() {
for step in steps {
results.extend(self.audit_composite_step(&step, config).await?);
}
}
Ok(results)
}
async fn audit_dependabot<'doc>(
&self,
_dependabot: &'doc Dependabot,
_config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
Ok(vec![])
}
async fn audit_raw<'doc>(
&self,
_input: &'doc AuditInput,
_config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
Ok(vec![])
}
#[instrument(skip(self, ident, config))]
async fn audit<'doc>(
&self,
ident: &'static str,
input: &'doc AuditInput,
config: &Config,
) -> Result<Vec<Finding<'doc>>, AuditError> {
if config.disables(ident) {
tracing::debug!(
"skipping: {ident} is disabled in config for group {group:?}",
group = input.key().group()
);
return Ok(vec![]);
}
let mut results = match input {
AuditInput::Workflow(workflow) => self.audit_workflow(workflow, config).await,
AuditInput::Action(action) => self.audit_action(action, config).await,
AuditInput::Dependabot(dependabot) => self.audit_dependabot(dependabot, config).await,
}?;
results.extend(self.audit_raw(input, config).await?);
Ok(results)
}
}