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

This commit is contained in:
William Woodruff 2025-12-04 22:15:33 -05:00 committed by GitHub
parent 0f386aa3c1
commit 7f8f24f90c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 691 additions and 28 deletions

View file

@ -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

View file

@ -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();
}

View 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

View 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)
}
}

View file

@ -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)?);
}
}

View file

@ -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()
}
}

View file

@ -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)?,
)
}

View file

@ -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)

View file

@ -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;

View file

@ -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)?,
);
}

View file

@ -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)?,
);
}

View file

@ -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)
}
}

View file

@ -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>(

View file

@ -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()

View file

@ -12,7 +12,7 @@ use crate::{
models::{
StepBodyCommon, StepCommon,
coordinate::{ActionCoordinate, ControlExpr, ControlFieldType, Toggle},
workflow::JobExt as _,
workflow::JobCommon as _,
},
state::AuditState,
utils,

View file

@ -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

View file

@ -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.
///

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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)
}

View 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(())
}

View file

@ -1,6 +1,7 @@
//! Per-audit integrationt tests, including snapshots.
mod anonymous_definition;
mod archived_uses;
mod artipacked;
mod bot_conditions;
mod cache_poisoning;

View 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

View file

@ -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

View file

@ -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 |

View file

@ -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

View 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
View 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()