mirror of
https://github.com/zizmorcore/zizmor.git
synced 2025-12-23 08:47:33 +00:00
feat: add an archived-uses audit (#1411)
Some checks failed
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
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Has been cancelled
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (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
CI / All tests pass (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Release (push) Has been cancelled
Some checks failed
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
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Has been cancelled
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (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
CI / All tests pass (push) Has been cancelled
zizmor wheel builds for PyPI 🐍 / Release (push) Has been cancelled
This commit is contained in:
parent
0f386aa3c1
commit
7f8f24f90c
28 changed files with 691 additions and 28 deletions
4
Makefile
4
Makefile
|
|
@ -42,6 +42,10 @@ codeql-injection-sinks: crates/zizmor/data/codeql-injection-sinks.json
|
|||
crates/zizmor/data/codeql-injection-sinks.json: support/codeql-injection-sinks.py
|
||||
$< > $@
|
||||
|
||||
.PHONY: archived-repos
|
||||
archived-repos:
|
||||
support/archived-repos.py
|
||||
|
||||
.PHONY: pinact
|
||||
pinact:
|
||||
pinact run --update --verify
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::fs::{self, File};
|
|||
use std::path::Path;
|
||||
use std::{env, io};
|
||||
|
||||
use fst::MapBuilder;
|
||||
use fst::{MapBuilder, SetBuilder};
|
||||
|
||||
fn do_context_capabilities() {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
|
|
@ -55,7 +55,29 @@ fn do_codeql_injection_sinks() {
|
|||
fs::copy(source, target).unwrap();
|
||||
}
|
||||
|
||||
fn do_archived_action_repos() {
|
||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
||||
let source = Path::new(&manifest_dir).join("data/archived-repos.txt");
|
||||
let target = Path::new(&env::var("OUT_DIR").unwrap()).join("archived-repos.fst");
|
||||
|
||||
print!(
|
||||
"cargo::rerun-if-changed={source}",
|
||||
source = source.display()
|
||||
);
|
||||
|
||||
let out = io::BufWriter::new(File::create(target).unwrap());
|
||||
let mut build = SetBuilder::new(out).unwrap();
|
||||
|
||||
let contents = fs::read_to_string(source).unwrap();
|
||||
for line in contents.lines() {
|
||||
build.insert(line).unwrap();
|
||||
}
|
||||
|
||||
build.finish().unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
do_context_capabilities();
|
||||
do_codeql_injection_sinks();
|
||||
do_archived_action_repos();
|
||||
}
|
||||
|
|
|
|||
87
crates/zizmor/data/archived-repos.txt
Normal file
87
crates/zizmor/data/archived-repos.txt
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
8398a7/action-slack
|
||||
actions-rs/audit-check
|
||||
actions-rs/cargo
|
||||
actions-rs/clippy-check
|
||||
actions-rs/components-nightly
|
||||
actions-rs/grcov
|
||||
actions-rs/install
|
||||
actions-rs/tarpaulin
|
||||
actions-rs/toolchain
|
||||
actions/create-release
|
||||
actions/setup-elixir
|
||||
actions/setup-haskell
|
||||
actions/setup-ruby
|
||||
actions/upload-release-asset
|
||||
andrewmcodes-archive/rubocop-linter-action
|
||||
artichoke/setup-rust
|
||||
aslafy-z/conventional-pr-title-action
|
||||
azure/appconfiguration-sync
|
||||
azure/appservice-actions
|
||||
azure/azure-resource-login-action
|
||||
azure/container-actions
|
||||
azure/container-scan
|
||||
azure/data-factory-deploy-action
|
||||
azure/data-factory-export-action
|
||||
azure/data-factory-validate-action
|
||||
azure/get-keyvault-secrets
|
||||
azure/k8s-actions
|
||||
azure/manage-azure-policy
|
||||
azure/publish-security-assessments
|
||||
azure/run-sqlpackage-action
|
||||
azure/spring-cloud-deploy
|
||||
azure/webapps-container-deploy
|
||||
cedrickring/golang-action
|
||||
cirrus-actions/rebase
|
||||
crazy-max/ghaction-docker-buildx
|
||||
decathlon/pull-request-labeler-action
|
||||
delaguardo/setup-graalvm
|
||||
dulvui/godot-android-export
|
||||
expo/expo-preview-action
|
||||
fabasoad/setup-zizmor-action
|
||||
facebook/pysa-action
|
||||
fregante/release-with-changelog
|
||||
google/mirror-branch-action
|
||||
google/skywater-pdk-actions
|
||||
gradle/gradle-build-action
|
||||
grafana/k6-action
|
||||
helaili/github-graphql-action
|
||||
helaili/jekyll-action
|
||||
ilshidur/action-slack
|
||||
jakejarvis/backblaze-b2-action
|
||||
jakejarvis/cloudflare-purge-action
|
||||
jakejarvis/firebase-deploy-action
|
||||
jakejarvis/hugo-build-action
|
||||
jakejarvis/lighthouse-action
|
||||
jakejarvis/s3-sync-action
|
||||
justinribeiro/lighthouse-action
|
||||
kanadgupta/glitch-sync
|
||||
kxxt/chatgpt-action
|
||||
machine-learning-apps/wandb-action
|
||||
mansagroup/gcs-cache-action
|
||||
marvinpinto/action-automatic-releases
|
||||
marvinpinto/actions
|
||||
maxheld83/ghpages
|
||||
micnncim/action-lgtm-reaction
|
||||
mikepenz/gradle-dependency-submission
|
||||
orf/cargo-bloat-action
|
||||
paambaati/codeclimate-action
|
||||
primer/figma-action
|
||||
repo-sync/pull-request
|
||||
repo-sync/repo-sync
|
||||
sagebind/docker-swarm-deploy-action
|
||||
scottbrenner/generate-changelog-action
|
||||
secrethub/actions
|
||||
semgrep/semgrep-action
|
||||
shaunlwm/action-release-debugapk
|
||||
sonarsource/sonarcloud-github-action
|
||||
stefanprodan/kube-tools
|
||||
swiftdocorg/github-wiki-publish-action
|
||||
tachiyomiorg/issue-moderator-action
|
||||
technote-space/auto-cancel-redundant-workflow
|
||||
technote-space/get-diff-action
|
||||
tencentcloudbase/cloudbase-action
|
||||
trmcnvn/chrome-addon
|
||||
whelk-io/maven-settings-xml-action
|
||||
yeslayla/build-godot-action
|
||||
youyo/aws-cdk-github-actions
|
||||
z0al/dependent-issues
|
||||
138
crates/zizmor/src/audit/archived_uses.rs
Normal file
138
crates/zizmor/src/audit/archived_uses.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use fst::Set;
|
||||
use github_actions_models::common::{RepositoryUses, Uses};
|
||||
use subfeature::Subfeature;
|
||||
|
||||
use crate::{
|
||||
audit::{Audit, AuditError, AuditLoadError, audit_meta},
|
||||
config::Config,
|
||||
finding::{Confidence, Finding, FindingBuilder, Persona, Severity, location::Locatable},
|
||||
models::{
|
||||
StepCommon as _,
|
||||
action::CompositeStep,
|
||||
workflow::{ReusableWorkflowCallJob, Step},
|
||||
},
|
||||
state::AuditState,
|
||||
};
|
||||
|
||||
static ARCHIVED_REPOS_FST: LazyLock<Set<&[u8]>> = LazyLock::new(|| {
|
||||
fst::Set::new(include_bytes!(concat!(env!("OUT_DIR"), "/archived-repos.fst")).as_slice())
|
||||
.expect("couldn't initialize archived repos FST")
|
||||
});
|
||||
|
||||
pub(crate) struct ArchivedUses;
|
||||
|
||||
audit_meta!(
|
||||
ArchivedUses,
|
||||
"archived-uses",
|
||||
"action or reusable workflow from archived repository"
|
||||
);
|
||||
|
||||
impl ArchivedUses {
|
||||
pub(crate) fn uses_is_archived<'doc>(uses: &RepositoryUses) -> Option<FindingBuilder<'doc>> {
|
||||
// TODO: Annoying that we need to allocate for case normalization here; can we use an
|
||||
// automaton to search the FST case-insensitively?
|
||||
let normalized = format!(
|
||||
"{owner}/{repo}",
|
||||
owner = uses.owner.to_lowercase(),
|
||||
repo = uses.repo.to_lowercase()
|
||||
);
|
||||
|
||||
ARCHIVED_REPOS_FST.contains(normalized.as_bytes()).then(|| {
|
||||
Self::finding()
|
||||
.confidence(Confidence::High)
|
||||
.severity(Severity::Medium)
|
||||
.persona(Persona::Regular)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Audit for ArchivedUses {
|
||||
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
async fn audit_step<'doc>(
|
||||
&self,
|
||||
step: &Step<'doc>,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>, AuditError> {
|
||||
let mut findings = vec![];
|
||||
|
||||
if let Some(Uses::Repository(uses)) = step.uses()
|
||||
&& let Some(finding) = Self::uses_is_archived(uses)
|
||||
{
|
||||
findings.push(
|
||||
finding
|
||||
.with_step(step)
|
||||
.add_location(
|
||||
step.location()
|
||||
.with_keys(["uses".into()])
|
||||
.subfeature(Subfeature::new(uses.owner.len(), uses.repo.as_str()))
|
||||
.annotated("repository is archived")
|
||||
.primary(),
|
||||
)
|
||||
.build(step)?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
async fn audit_composite_step<'doc>(
|
||||
&self,
|
||||
step: &CompositeStep<'doc>,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>, AuditError> {
|
||||
let mut findings = vec![];
|
||||
|
||||
if let Some(Uses::Repository(uses)) = step.uses()
|
||||
&& let Some(finding) = Self::uses_is_archived(uses)
|
||||
{
|
||||
findings.push(
|
||||
finding
|
||||
.with_step(step)
|
||||
.add_location(
|
||||
step.location()
|
||||
.with_keys(["uses".into()])
|
||||
.annotated("repository is archived")
|
||||
.primary(),
|
||||
)
|
||||
.build(step)?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
|
||||
async fn audit_reusable_job<'doc>(
|
||||
&self,
|
||||
job: &ReusableWorkflowCallJob<'doc>,
|
||||
_config: &Config,
|
||||
) -> Result<Vec<Finding<'doc>>, AuditError> {
|
||||
let mut findings = vec![];
|
||||
|
||||
if let Uses::Repository(uses) = &job.uses
|
||||
&& let Some(finding) = Self::uses_is_archived(uses)
|
||||
{
|
||||
findings.push(
|
||||
finding
|
||||
.with_job(job)
|
||||
.add_location(
|
||||
job.location()
|
||||
.with_keys(["uses".into()])
|
||||
.annotated("repository is archived")
|
||||
.primary(),
|
||||
)
|
||||
.build(job)?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ use super::{Audit, AuditLoadError, AuditState, audit_meta};
|
|||
use crate::{
|
||||
audit::AuditError,
|
||||
finding::{Confidence, Fix, FixDisposition, Severity, location::Locatable as _},
|
||||
models::workflow::{JobExt, Workflow},
|
||||
models::workflow::{JobCommon, Workflow},
|
||||
utils::{self, ExtractedExpr},
|
||||
};
|
||||
use subfeature::Subfeature;
|
||||
|
|
@ -130,7 +130,7 @@ impl Audit for BotConditions {
|
|||
finding_builder = finding_builder.fix(fix);
|
||||
}
|
||||
|
||||
findings.push(finding_builder.build(job.parent())?);
|
||||
findings.push(finding_builder.build(job)?);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::finding::location::{Locatable as _, Routable};
|
|||
use crate::finding::{Confidence, Finding, Fix, FixDisposition, Severity};
|
||||
use crate::models::StepCommon;
|
||||
use crate::models::coordinate::{ActionCoordinate, ControlExpr, ControlFieldType, Toggle, Usage};
|
||||
use crate::models::workflow::{JobExt as _, NormalJob, Step, Steps};
|
||||
use crate::models::workflow::{JobCommon as _, NormalJob, Step, Steps};
|
||||
use crate::state::AuditState;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
|
@ -444,7 +444,7 @@ impl CachePoisoning {
|
|||
finding_builder = finding_builder.fix(fix);
|
||||
}
|
||||
|
||||
finding_builder.build(step.workflow()).ok()
|
||||
finding_builder.build(step).ok()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::config::Config;
|
|||
use crate::finding::location::Locatable as _;
|
||||
use crate::finding::{Confidence, Finding, Severity};
|
||||
use crate::models::StepCommon;
|
||||
use crate::models::{workflow::JobExt as _, workflow::Step};
|
||||
use crate::models::{workflow::JobCommon as _, workflow::Step};
|
||||
use crate::state::AuditState;
|
||||
use crate::utils;
|
||||
use crate::utils::once::static_regex;
|
||||
|
|
@ -419,7 +419,7 @@ impl Audit for GitHubEnv {
|
|||
.with_keys(["run".into()])
|
||||
.annotated(format!("write to {dest} may allow code execution")),
|
||||
)
|
||||
.build(step.workflow())?,
|
||||
.build(step)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -463,7 +463,7 @@ impl Audit for GitHubEnv {
|
|||
.with_keys(["run".into()])
|
||||
.annotated(format!("write to {dest} may allow code execution")),
|
||||
)
|
||||
.build(step.action())?,
|
||||
.build(step)?,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ impl Audit for ImpostorCommit {
|
|||
finding_builder = finding_builder.fix(fix);
|
||||
}
|
||||
|
||||
findings.push(finding_builder.build(step.action()).map_err(Self::err)?);
|
||||
findings.push(finding_builder.build(step).map_err(Self::err)?);
|
||||
}
|
||||
|
||||
Ok(findings)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ use crate::{
|
|||
};
|
||||
|
||||
pub(crate) mod anonymous_definition;
|
||||
pub(crate) mod archived_uses;
|
||||
pub(crate) mod artipacked;
|
||||
pub(crate) mod bot_conditions;
|
||||
pub(crate) mod cache_poisoning;
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ impl Audit for RefConfusion {
|
|||
.with_keys(["uses".into()])
|
||||
.annotated(REF_CONFUSION_ANNOTATION),
|
||||
)
|
||||
.build(step.action())
|
||||
.build(step)
|
||||
.map_err(Self::err)?,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use super::{Audit, AuditLoadError, AuditState, audit_meta};
|
|||
use crate::{
|
||||
audit::AuditError,
|
||||
finding::{Confidence, location::Locatable as _},
|
||||
models::workflow::JobExt as _,
|
||||
};
|
||||
|
||||
pub(crate) struct SecretsInherit;
|
||||
|
|
@ -47,7 +46,7 @@ impl Audit for SecretsInherit {
|
|||
)
|
||||
.confidence(Confidence::High)
|
||||
.severity(crate::finding::Severity::Medium)
|
||||
.build(job.parent())?,
|
||||
.build(job)?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ use crate::{
|
|||
Confidence, Finding, Persona, Severity,
|
||||
location::{Locatable as _, SymbolicLocation},
|
||||
},
|
||||
models::workflow::JobExt as _,
|
||||
state::AuditState,
|
||||
};
|
||||
|
||||
|
|
@ -30,7 +29,7 @@ impl UnpinnedImages {
|
|||
.confidence(Confidence::High)
|
||||
.add_location(annotated_location)
|
||||
.persona(persona)
|
||||
.build(job.parent())
|
||||
.build(job)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use crate::{
|
|||
Confidence, Fix, FixDisposition, Severity,
|
||||
location::{Locatable as _, SymbolicLocation},
|
||||
},
|
||||
models::{AsDocument, workflow::JobExt},
|
||||
models::AsDocument,
|
||||
utils,
|
||||
};
|
||||
use yamlpatch::{Op, Patch};
|
||||
|
|
@ -158,7 +158,7 @@ impl Audit for UnsoundCondition {
|
|||
job: &crate::models::workflow::NormalJob<'doc>,
|
||||
_config: &crate::config::Config,
|
||||
) -> Result<Vec<crate::finding::Finding<'doc>>, AuditError> {
|
||||
self.process_conditions(job.parent(), job.conditions())
|
||||
self.process_conditions(job, job.conditions())
|
||||
}
|
||||
|
||||
async fn audit_reusable_job<'doc>(
|
||||
|
|
@ -167,7 +167,7 @@ impl Audit for UnsoundCondition {
|
|||
_config: &crate::config::Config,
|
||||
) -> Result<Vec<crate::finding::Finding<'doc>>, AuditError> {
|
||||
let conds = job.r#if.iter().map(|cond| (cond, job.location()));
|
||||
self.process_conditions(job.parent(), conds)
|
||||
self.process_conditions(job, conds)
|
||||
}
|
||||
|
||||
async fn audit_action<'doc>(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ use super::{Audit, AuditLoadError, AuditState, audit_meta};
|
|||
use crate::{
|
||||
audit::AuditError,
|
||||
finding::{Confidence, Severity},
|
||||
models::workflow::JobExt as _,
|
||||
utils::{self, ExtractedExpr},
|
||||
};
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ impl Audit for UnsoundContains {
|
|||
.primary()
|
||||
.annotated(format!("contains(..) condition can be bypassed if attacker can control '{context}'")),
|
||||
)
|
||||
.build(job.parent())
|
||||
.build(job)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
models::{
|
||||
StepBodyCommon, StepCommon,
|
||||
coordinate::{ActionCoordinate, ControlExpr, ControlFieldType, Toggle},
|
||||
workflow::JobExt as _,
|
||||
workflow::JobCommon as _,
|
||||
},
|
||||
state::AuditState,
|
||||
utils,
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ use clap::ValueEnum;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::location::{Location, SymbolicLocation};
|
||||
use crate::{InputKey, audit::AuditError, models::AsDocument, registry::input::Group};
|
||||
use crate::{
|
||||
InputKey,
|
||||
audit::AuditError,
|
||||
models::{AsDocument, StepCommon, workflow::JobCommon},
|
||||
registry::input::Group,
|
||||
};
|
||||
use yamlpatch::{self, Patch};
|
||||
|
||||
pub(crate) mod location;
|
||||
|
|
@ -223,6 +228,44 @@ impl<'doc> FindingBuilder<'doc> {
|
|||
self
|
||||
}
|
||||
|
||||
/// Add a "useful" location for the given step, if it has a name or ID.
|
||||
pub(crate) fn with_step(mut self, step: &impl StepCommon<'doc>) -> Self {
|
||||
if step.name().is_some() {
|
||||
self.locations.push(
|
||||
step.location()
|
||||
.with_keys(["name".into()])
|
||||
.annotated("this step"),
|
||||
);
|
||||
} else if step.id().is_some() {
|
||||
self.locations.push(
|
||||
step.location()
|
||||
.with_keys(["id".into()])
|
||||
.annotated("this step"),
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn with_job(mut self, job: &impl JobCommon<'doc>) -> Self {
|
||||
if job.name().is_some() {
|
||||
self.locations.push(
|
||||
job.location()
|
||||
.with_keys(["name".into()])
|
||||
.annotated("this job"),
|
||||
);
|
||||
} else {
|
||||
self.locations.push(
|
||||
job.parent()
|
||||
.location()
|
||||
.with_keys(["jobs".into(), job.id().into()])
|
||||
.annotated("this job"),
|
||||
);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn tip(mut self, tip: impl Into<String>) -> Self {
|
||||
self.tip = Some(tip.into());
|
||||
self
|
||||
|
|
|
|||
|
|
@ -37,6 +37,12 @@ pub(crate) enum StepBodyCommon<'s> {
|
|||
|
||||
/// Common interfaces between workflow and action steps.
|
||||
pub(crate) trait StepCommon<'doc>: Locatable<'doc> + HasInputs {
|
||||
/// Returns the step's name, if present.
|
||||
fn name(&self) -> Option<&'doc str>;
|
||||
|
||||
/// Returns the step's ID, if present.
|
||||
fn id(&self) -> Option<&'doc str>;
|
||||
|
||||
/// Returns the step's index within its parent job or action.
|
||||
fn index(&self) -> usize;
|
||||
|
||||
|
|
@ -45,7 +51,7 @@ pub(crate) trait StepCommon<'doc>: Locatable<'doc> + HasInputs {
|
|||
fn env_is_static(&self, ctx: &context::Context) -> bool;
|
||||
|
||||
/// Returns a [`common::Uses`] for this step, if it has one.
|
||||
fn uses(&self) -> Option<&common::Uses>;
|
||||
fn uses(&self) -> Option<&'doc common::Uses>;
|
||||
|
||||
/// Returns this step's job's strategy, if present.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -190,6 +190,14 @@ impl HasInputs for CompositeStep<'_> {
|
|||
}
|
||||
|
||||
impl<'doc> StepCommon<'doc> for CompositeStep<'doc> {
|
||||
fn name(&self) -> Option<&'doc str> {
|
||||
self.name.as_deref()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'doc str> {
|
||||
self.id.as_deref()
|
||||
}
|
||||
|
||||
fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
|
@ -198,7 +206,7 @@ impl<'doc> StepCommon<'doc> for CompositeStep<'doc> {
|
|||
utils::env_is_static(ctx, &[&self.env])
|
||||
}
|
||||
|
||||
fn uses(&self) -> Option<&common::Uses> {
|
||||
fn uses(&self) -> Option<&'doc common::Uses> {
|
||||
let action::StepBody::Uses { uses, .. } = &self.inner.body else {
|
||||
return None;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -286,7 +286,13 @@ impl<'doc> NormalJob<'doc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'doc> JobExt<'doc> for NormalJob<'doc> {
|
||||
impl<'a, 'doc> AsDocument<'a, 'doc> for NormalJob<'doc> {
|
||||
fn as_document(&'a self) -> &'doc yamlpath::Document {
|
||||
self.parent.as_document()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'doc> JobCommon<'doc> for NormalJob<'doc> {
|
||||
fn id(&self) -> &'doc str {
|
||||
self.id
|
||||
}
|
||||
|
|
@ -329,7 +335,13 @@ impl<'doc> ReusableWorkflowCallJob<'doc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'doc> JobExt<'doc> for ReusableWorkflowCallJob<'doc> {
|
||||
impl<'a, 'doc> AsDocument<'a, 'doc> for ReusableWorkflowCallJob<'doc> {
|
||||
fn as_document(&'a self) -> &'doc yamlpath::Document {
|
||||
self.parent.as_document()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'doc> JobCommon<'doc> for ReusableWorkflowCallJob<'doc> {
|
||||
fn id(&self) -> &'doc str {
|
||||
self.id
|
||||
}
|
||||
|
|
@ -352,7 +364,7 @@ impl<'doc> std::ops::Deref for ReusableWorkflowCallJob<'doc> {
|
|||
}
|
||||
|
||||
/// Common behavior across both normal and reusable jobs.
|
||||
pub(crate) trait JobExt<'doc> {
|
||||
pub(crate) trait JobCommon<'doc>: Locatable<'doc> {
|
||||
/// The job's unique ID (i.e., its key in the workflow's `jobs:` block).
|
||||
fn id(&self) -> &'doc str;
|
||||
|
||||
|
|
@ -363,7 +375,7 @@ pub(crate) trait JobExt<'doc> {
|
|||
fn parent(&self) -> &'doc Workflow;
|
||||
}
|
||||
|
||||
impl<'doc, T: JobExt<'doc>> Locatable<'doc> for T {
|
||||
impl<'doc, T: JobCommon<'doc>> Locatable<'doc> for T {
|
||||
/// Returns this job's [`SymbolicLocation`].
|
||||
fn location(&self) -> SymbolicLocation<'doc> {
|
||||
self.parent()
|
||||
|
|
@ -632,6 +644,14 @@ impl HasInputs for Step<'_> {
|
|||
}
|
||||
|
||||
impl<'doc> StepCommon<'doc> for Step<'doc> {
|
||||
fn name(&self) -> Option<&'doc str> {
|
||||
self.inner.name.as_deref()
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'doc str> {
|
||||
self.inner.id.as_deref()
|
||||
}
|
||||
|
||||
fn index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
|
@ -640,7 +660,7 @@ impl<'doc> StepCommon<'doc> for Step<'doc> {
|
|||
utils::env_is_static(ctx, &[&self.env, &self.job().env, &self.workflow().env])
|
||||
}
|
||||
|
||||
fn uses(&self) -> Option<&common::Uses> {
|
||||
fn uses(&self) -> Option<&'doc common::Uses> {
|
||||
let StepBody::Uses { uses, .. } = &self.inner.body else {
|
||||
return None;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ impl AuditRegistry {
|
|||
register_audit!(audit::dependabot_execution::DependabotExecution);
|
||||
register_audit!(audit::dependabot_cooldown::DependabotCooldown);
|
||||
register_audit!(audit::concurrency_limits::ConcurrencyLimits);
|
||||
register_audit!(audit::archived_uses::ArchivedUses);
|
||||
|
||||
Ok(registry)
|
||||
}
|
||||
|
|
|
|||
67
crates/zizmor/tests/integration/audit/archived_uses.rs
Normal file
67
crates/zizmor/tests/integration/audit/archived_uses.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
use crate::common::{input_under_test, zizmor};
|
||||
|
||||
#[test]
|
||||
fn test_regular_persona() -> anyhow::Result<()> {
|
||||
insta::assert_snapshot!(
|
||||
zizmor().input(input_under_test("archived-uses.yml")).run()?,
|
||||
@r"
|
||||
warning[archived-uses]: action or reusable workflow from archived repository
|
||||
--> @@INPUT@@:17:23
|
||||
|
|
||||
16 | - name: setup ruby
|
||||
| ---------------- this step
|
||||
17 | uses: actions/setup-ruby@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
| ^^^^^^^^^^ repository is archived
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
warning[archived-uses]: action or reusable workflow from archived repository
|
||||
--> @@INPUT@@:20:23
|
||||
|
|
||||
19 | - name: SETUP RUBY BUT LOUDLY
|
||||
| --------------------------- this step
|
||||
20 | uses: ACTIONS/SETUP-RUBY@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
| ^^^^^^^^^^ repository is archived
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
warning[archived-uses]: action or reusable workflow from archived repository
|
||||
--> @@INPUT@@:24:5
|
||||
|
|
||||
23 | name: archived-uses-reusable
|
||||
| ---------------------------- this job
|
||||
24 | uses: actions/setup-ruby/.github/workflows/notreal.yml@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ repository is archived
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
3 findings: 0 informational, 0 low, 3 medium, 0 high
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_composite_action() -> anyhow::Result<()> {
|
||||
insta::assert_snapshot!(
|
||||
zizmor()
|
||||
.input(input_under_test("archived-uses/action/"))
|
||||
.run()?,
|
||||
@r"
|
||||
warning[archived-uses]: action or reusable workflow from archived repository
|
||||
--> @@INPUT@@action.yml:9:7
|
||||
|
|
||||
8 | - name: setup ruby
|
||||
| ---------------- this step
|
||||
9 | uses: actions/setup-ruby@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ repository is archived
|
||||
|
|
||||
= note: audit confidence → High
|
||||
|
||||
1 finding: 0 informational, 0 low, 1 medium, 0 high
|
||||
"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
//! Per-audit integrationt tests, including snapshots.
|
||||
|
||||
mod anonymous_definition;
|
||||
mod archived_uses;
|
||||
mod artipacked;
|
||||
mod bot_conditions;
|
||||
mod cache_poisoning;
|
||||
|
|
|
|||
24
crates/zizmor/tests/integration/test-data/archived-uses.yml
Normal file
24
crates/zizmor/tests/integration/test-data/archived-uses.yml
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: archived-uses
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
archived-uses:
|
||||
name: archived-uses
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: setup ruby
|
||||
uses: actions/setup-ruby@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
|
||||
- name: SETUP RUBY BUT LOUDLY
|
||||
uses: ACTIONS/SETUP-RUBY@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
|
||||
archived-uses-reusable:
|
||||
name: archived-uses-reusable
|
||||
uses: actions/setup-ruby/.github/workflows/notreal.yml@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
name: archived-uses
|
||||
|
||||
description: archived-uses composite action
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: setup ruby
|
||||
uses: actions/setup-ruby@e932e7af67fc4a8fc77bd86b744acd4e42fe3543 # v1.1.3
|
||||
|
|
@ -60,6 +60,56 @@ Add a `name:` field to your workflow or action.
|
|||
- run: echo "Hello!"
|
||||
```
|
||||
|
||||
## `archived-uses`
|
||||
|
||||
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable |
|
||||
|----------|------------------|---------------|----------------|--------------------|--------------|
|
||||
| Workflow, Action | [archived-uses.yml] | v1.19.0 | ✅ | ❌ | ❌ |
|
||||
|
||||
[archived-uses.yml]: https://github.com/zizmorcore/zizmor/blob/main/crates/zizmor/tests/integration/test-data/archived-uses.yml
|
||||
|
||||
|
||||
Detects `#!yaml uses:` clauses that reference [archived repositories].
|
||||
|
||||
[archived repositories]: https://docs.github.com/en/repositories/archiving-a-github-repository/archiving-repositories
|
||||
|
||||
Archival on GitHub makes a repository read-only, and indicates that the
|
||||
repository is no longer maintained. Using actions or reusable workflows from archived
|
||||
represents a supply chain risk:
|
||||
|
||||
- Unmaintained repositories are more likely to accumulate indirect vulnerabilties,
|
||||
including in any dependencies that have been vendored into JavaScript actions
|
||||
(or that are used indirectly through transitive dependencies that have gone
|
||||
stale).
|
||||
|
||||
- Any vulnerabilities discovered in the action or reusable workflow *itself*
|
||||
are unlikely to be fixed, since the repository is read-only.
|
||||
|
||||
Consequently, users are encouraged to avoid dependening on archived repositories
|
||||
for actions or reusable workflows.
|
||||
|
||||
### Remediation
|
||||
|
||||
Depending on the archived repository's functionality, you may be able to:
|
||||
|
||||
- _Remove_ the action/reusable workflow entirely. Actions @actions-rs/cargo,
|
||||
for example, can be replaced by directly invoking the correct `#!bash cargo ...`
|
||||
command in a `#!yaml run:` step.
|
||||
|
||||
- _Replace_ the archived action/reusable workflow with a maintained alternative.
|
||||
For example, @actions/setup-ruby can be replaced with @ruby/setup-ruby.
|
||||
|
||||
!!! tip
|
||||
|
||||
Many archived actions are thin wrappers around GitHub's REST and GraphQL
|
||||
APIs. In most cases, you can replace these actions with usage of the
|
||||
[`gh` CLI](https://cli.github.com/), which is pre-installed on GitHub-hosted
|
||||
runners.
|
||||
|
||||
For more information, see [Using GitHub CLI in workflows].
|
||||
|
||||
[Using GitHub CLI in workflows]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-github-cli
|
||||
|
||||
## `artipacked`
|
||||
|
||||
| Type | Examples | Introduced in | Works offline | Auto-fixes available | Configurable |
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ of `zizmor`.
|
|||
|
||||
## Next (UNRELEASED)
|
||||
|
||||
### New Features 🌈
|
||||
|
||||
* **New audit**: [archived-uses] detects usages of archived repositories in
|
||||
`#!yaml uses:` clauses (#1411)
|
||||
|
||||
### Enhancements 🌱
|
||||
|
||||
* The [use-trusted-publishing] audit now detects additional publishing command
|
||||
|
|
@ -1311,5 +1316,6 @@ This is one of `zizmor`'s bigger recent releases! Key enhancements include:
|
|||
[dependabot-execution]: ./audits.md#dependabot-execution
|
||||
[dependabot-cooldown]: ./audits.md#dependabot-cooldown
|
||||
[concurrency-limits]: ./audits.md#concurrency-limits
|
||||
[archived-uses]: ./audits.md#archived-uses
|
||||
|
||||
[exit code]: ./usage.md#exit-codes
|
||||
|
|
|
|||
147
support/archived-action-repos.txt
Normal file
147
support/archived-action-repos.txt
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
# archived-action-repos.txt
|
||||
# one per line, comment lines begin with #
|
||||
#
|
||||
# NOTE(ww): I somewhat aritrarily curated this manually from a GitHub search
|
||||
# for `topic:github-actions archived:true` as of 2025-12-04.
|
||||
|
||||
# official actions
|
||||
actions/upload-release-asset
|
||||
actions/create-release
|
||||
actions/setup-ruby
|
||||
actions/setup-elixir
|
||||
actions/setup-haskell
|
||||
|
||||
|
||||
# community actions
|
||||
actions-rs/cargo
|
||||
actions-rs/grcov
|
||||
actions-rs/audit-check
|
||||
actions-rs/toolchain
|
||||
actions-rs/tarpaulin
|
||||
actions-rs/clippy-check
|
||||
actions-rs/install
|
||||
actions-rs/components-nightly
|
||||
|
||||
andrewmcodes-archive/rubocop-linter-action
|
||||
|
||||
artichoke/setup-rust
|
||||
|
||||
aslafy-z/conventional-pr-title-action
|
||||
|
||||
Azure/AppConfiguration-Sync
|
||||
Azure/appservice-actions
|
||||
Azure/azure-resource-login-action
|
||||
Azure/container-actions
|
||||
Azure/container-scan
|
||||
Azure/get-keyvault-secrets
|
||||
Azure/k8s-actions
|
||||
Azure/manage-azure-policy
|
||||
Azure/data-factory-deploy-action
|
||||
Azure/data-factory-export-action
|
||||
Azure/data-factory-validate-action
|
||||
Azure/publish-security-assessments
|
||||
Azure/run-sqlpackage-action
|
||||
Azure/spring-cloud-deploy
|
||||
Azure/webapps-container-deploy
|
||||
|
||||
cedrickring/golang-action
|
||||
|
||||
cirrus-actions/rebase
|
||||
|
||||
crazy-max/ghaction-docker-buildx
|
||||
|
||||
Decathlon/pull-request-labeler-action
|
||||
|
||||
DeLaGuardo/setup-graalvm
|
||||
|
||||
dulvui/godot-android-export
|
||||
|
||||
expo/expo-preview-action
|
||||
|
||||
fabasoad/setup-zizmor-action
|
||||
|
||||
facebook/pysa-action
|
||||
|
||||
fregante/release-with-changelog
|
||||
|
||||
google/mirror-branch-action
|
||||
google/skywater-pdk-actions
|
||||
|
||||
gradle/gradle-build-action
|
||||
|
||||
grafana/k6-action
|
||||
|
||||
helaili/github-graphql-action
|
||||
helaili/jekyll-action
|
||||
|
||||
Ilshidur/action-slack
|
||||
|
||||
jakejarvis/backblaze-b2-action
|
||||
jakejarvis/cloudflare-purge-action
|
||||
jakejarvis/firebase-deploy-action
|
||||
jakejarvis/hugo-build-action
|
||||
jakejarvis/lighthouse-action
|
||||
jakejarvis/s3-sync-action
|
||||
|
||||
justinribeiro/lighthouse-action
|
||||
|
||||
kanadgupta/glitch-sync
|
||||
|
||||
kxxt/chatgpt-action
|
||||
|
||||
machine-learning-apps/wandb-action
|
||||
|
||||
MansaGroup/gcs-cache-action
|
||||
|
||||
marvinpinto/actions
|
||||
marvinpinto/action-automatic-releases
|
||||
|
||||
maxheld83/ghpages
|
||||
|
||||
micnncim/action-lgtm-reaction
|
||||
|
||||
mikepenz/gradle-dependency-submission
|
||||
|
||||
orf/cargo-bloat-action
|
||||
|
||||
paambaati/codeclimate-action
|
||||
|
||||
primer/figma-action
|
||||
|
||||
repo-sync/pull-request
|
||||
repo-sync/repo-sync
|
||||
|
||||
sagebind/docker-swarm-deploy-action
|
||||
|
||||
ScottBrenner/generate-changelog-action
|
||||
|
||||
secrethub/actions
|
||||
|
||||
semgrep/semgrep-action
|
||||
|
||||
ShaunLWM/action-release-debugapk
|
||||
|
||||
stefanprodan/kube-tools
|
||||
|
||||
SonarSource/sonarcloud-github-action
|
||||
|
||||
SwiftDocOrg/github-wiki-publish-action
|
||||
|
||||
tachiyomiorg/issue-moderator-action
|
||||
|
||||
technote-space/auto-cancel-redundant-workflow
|
||||
technote-space/get-diff-action
|
||||
|
||||
TencentCloudBase/cloudbase-action
|
||||
|
||||
trmcnvn/chrome-addon
|
||||
|
||||
whelk-io/maven-settings-xml-action
|
||||
|
||||
yeslayla/build-godot-action
|
||||
|
||||
youyo/aws-cdk-github-actions
|
||||
|
||||
z0al/dependent-issues
|
||||
|
||||
8398a7/action-slack
|
||||
32
support/archived-repos.py
Executable file
32
support/archived-repos.py
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.14"
|
||||
# dependencies = []
|
||||
# ///
|
||||
from pathlib import Path
|
||||
|
||||
_HERE = Path(__file__).parent
|
||||
_ARCHIVED_ACTION_REPOS = _HERE / "archived-action-repos.txt"
|
||||
|
||||
assert _ARCHIVED_ACTION_REPOS.is_file(), f"Missing {_ARCHIVED_ACTION_REPOS}"
|
||||
|
||||
_OUT = _HERE.parent / "crates" / "zizmor" / "data" / "archived-repos.txt"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
lines = []
|
||||
for line in _ARCHIVED_ACTION_REPOS.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
|
||||
lines.append(line.lower())
|
||||
|
||||
lines.sort()
|
||||
|
||||
with _OUT.open("w") as io:
|
||||
print("\n".join(lines), file=io)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue