feat: begin scaffolding for Dependabot support (#1215)
Some checks are pending
Benchmark baseline / Continuous Benchmarking with Bencher (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Test (push) Waiting to run
CI / Test site build (push) Waiting to run
CI / All tests pass (push) Blocked by required conditions
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (manylinux) (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build Linux wheels (musllinux) (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Release (push) Blocked by required conditions
zizmor wheel builds for PyPI 🐍 / Build Windows wheels (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build macOS wheels (push) Waiting to run
zizmor wheel builds for PyPI 🐍 / Build source distribution (push) Waiting to run
Deploy zizmor documentation site 🌐 / Deploy zizmor documentation to GitHub Pages 🌐 (push) Waiting to run
GitHub Actions Security Analysis with zizmor 🌈 / Run zizmor 🌈 (push) Waiting to run

This commit is contained in:
William Woodruff 2025-10-06 17:46:17 -04:00 committed by GitHub
parent bee86b1bb2
commit fbd86b5955
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1442 additions and 10 deletions

View file

@ -30,6 +30,7 @@ docs/snippets/sponsors.html: docs/snippets/sponsors.json docs/snippets/render-sp
refresh-schemas:
curl https://www.schemastore.org/github-workflow.json > crates/zizmor/src/data/github-workflow.json
curl https://www.schemastore.org/github-action.json > crates/zizmor/src/data/github-action.json
curl https://www.schemastore.org/dependabot-2.0.json > crates/zizmor/src/data/dependabot-2.0.json
.PHONY: webhooks-to-contexts
webhooks-to-contexts:

View file

@ -8,7 +8,8 @@ github-actions-models
[![GitHub Sponsors](https://img.shields.io/github/sponsors/woodruffw?style=flat&logo=githubsponsors&labelColor=white&color=white)](https://github.com/sponsors/woodruffw)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/PGU3zGZuGG)
Unofficial, high-quality data models for GitHub Actions workflows, actions, and related components.
Unofficial, high-quality data models for GitHub Actions workflows, actions, and
Dependabot configuration files.
## Why?

View file

@ -88,6 +88,26 @@ pub enum Registry {
},
}
/// Cooldown settings for Dependabot updates.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
pub struct Cooldown {
pub default_days: Option<u64>,
pub semver_major_days: Option<u64>,
pub semver_minor_days: Option<u64>,
pub semver_patch_days: Option<u64>,
pub include: Vec<String>,
pub exclude: Vec<String>,
}
/// A `directory` or `directories` field in a Dependabot `update` directive.
#[derive(Deserialize, Debug, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum Directories {
Directory(String),
Directories(Vec<String>),
}
/// A single `update` directive.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
@ -97,7 +117,9 @@ pub struct Update {
#[serde(default)]
pub assignees: IndexSet<String>,
pub commit_message: Option<CommitMessage>,
pub directory: String,
pub cooldown: Option<Cooldown>,
#[serde(flatten)]
pub directories: Directories,
#[serde(default)]
pub groups: IndexMap<String, Group>,
#[serde(default)]

View file

@ -0,0 +1,20 @@
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates#setting-up-a-cooldown-period-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
cooldown:
default-days: 5
semver-major-days: 30
semver-minor-days: 7
semver-patch-days: 3
include:
- "requests"
- "numpy"
- "pandas*"
- "django"
exclude:
- "pandas"

View file

@ -0,0 +1,40 @@
# https://github.com/grafana/grafana/blob/0de6d103c286ae8c0380dd420dfaec24ee706fe9/.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directories:
- "/"
- "/apps/playlist"
- "/apps/secret"
- "/apps/investigations"
- "/pkg/aggregator"
- "/pkg/apimachinery"
- "/pkg/apis/folder"
- "/pkg/apiserver"
- "/pkg/build"
- "/pkg/build/wire"
- "/pkg/promlib"
- "/pkg/semconv"
- "/pkg/storage/unified/apistore"
- "/pkg/storage/unified/resource"
- "/pkg/util/xorm"
schedule:
interval: "daily"
time: "02:00"
timezone: Etc/UTC
open-pull-requests-limit: 10
- package-ecosystem: "docker"
directories:
- "/"
- "/packaging/docker/custom"
- "/scripts/verify-repo-update"
schedule:
interval: "daily"
time: "02:00"
timezone: Etc/UTC
open-pull-requests-limit: 10

View file

@ -1,7 +1,7 @@
use std::path::Path;
use github_actions_models::dependabot::v2::{
Dependabot, Interval, PackageEcosystem, RebaseStrategy,
Dependabot, Directories, Interval, PackageEcosystem, RebaseStrategy,
};
use indexmap::IndexSet;
@ -33,7 +33,7 @@ fn test_contents() {
let pip = &dependabot.updates[0];
assert_eq!(pip.package_ecosystem, PackageEcosystem::Pip);
assert_eq!(pip.directory, "/");
assert_eq!(pip.directories, Directories::Directory("/".into()));
assert_eq!(pip.schedule.interval, Interval::Daily);
assert_eq!(pip.open_pull_requests_limit, 5); // default
@ -42,7 +42,10 @@ fn test_contents() {
github_actions.package_ecosystem,
PackageEcosystem::GithubActions
);
assert_eq!(github_actions.directory, "/");
assert_eq!(
github_actions.directories,
Directories::Directory("/".into())
);
assert_eq!(github_actions.open_pull_requests_limit, 99);
assert_eq!(github_actions.rebase_strategy, RebaseStrategy::Disabled);
assert_eq!(github_actions.groups.len(), 1);
@ -56,7 +59,10 @@ fn test_contents() {
github_actions.package_ecosystem,
PackageEcosystem::GithubActions
);
assert_eq!(github_actions.directory, ".github/actions/upload-coverage/");
assert_eq!(
github_actions.directories,
Directories::Directory(".github/actions/upload-coverage/".into())
);
assert_eq!(github_actions.open_pull_requests_limit, 99);
assert_eq!(github_actions.rebase_strategy, RebaseStrategy::Disabled);
assert_eq!(github_actions.groups.len(), 1);

View file

@ -10,6 +10,7 @@ use crate::{
models::{
AsDocument,
action::{Action, CompositeStep},
dependabot::Dependabot,
workflow::{Job, NormalJob, ReusableWorkflowCallJob, Step, Workflow},
},
registry::input::InputKey,
@ -48,6 +49,7 @@ pub(crate) mod use_trusted_publishing;
pub(crate) enum AuditInput {
Workflow(Workflow),
Action(Action),
Dependabot(Dependabot),
}
impl AuditInput {
@ -55,6 +57,7 @@ impl AuditInput {
match self {
AuditInput::Workflow(workflow) => &workflow.key,
AuditInput::Action(action) => &action.key,
AuditInput::Dependabot(dependabot) => &dependabot.key,
}
}
@ -62,6 +65,7 @@ impl AuditInput {
match self {
AuditInput::Workflow(workflow) => workflow.link.as_deref(),
AuditInput::Action(action) => action.link.as_deref(),
AuditInput::Dependabot(dependabot) => dependabot.link.as_deref(),
}
}
@ -69,6 +73,7 @@ impl AuditInput {
match self {
AuditInput::Workflow(workflow) => workflow.location(),
AuditInput::Action(action) => action.location(),
AuditInput::Dependabot(dependabot) => dependabot.location(),
}
}
}
@ -78,6 +83,7 @@ impl<'a> AsDocument<'a, 'a> for AuditInput {
match self {
AuditInput::Workflow(workflow) => workflow.as_document(),
AuditInput::Action(action) => action.as_document(),
AuditInput::Dependabot(dependabot) => dependabot.as_document(),
}
}
}
@ -94,6 +100,12 @@ impl From<Action> for AuditInput {
}
}
impl From<Dependabot> for AuditInput {
fn from(value: Dependabot) -> Self {
Self::Dependabot(value)
}
}
/// A supertrait for all audits.
///
/// Workflow audits, action audits, and all future audit types
@ -270,6 +282,14 @@ pub(crate) trait Audit: AuditCore {
Ok(results)
}
fn audit_dependabot<'doc>(
&self,
_dependabot: &'doc Dependabot,
_config: &Config,
) -> anyhow::Result<Vec<Finding<'doc>>> {
Ok(vec![])
}
fn audit_raw<'doc>(
&self,
_input: &'doc AuditInput,
@ -308,6 +328,7 @@ pub(crate) trait Audit: AuditCore {
let mut results = match input {
AuditInput::Workflow(workflow) => self.audit_workflow(workflow, config),
AuditInput::Action(action) => self.audit_action(action, config),
AuditInput::Dependabot(dependabot) => self.audit_dependabot(dependabot, config),
}?;
results.extend(self.audit_raw(input, config)?);

File diff suppressed because it is too large Load diff

View file

@ -633,6 +633,14 @@ impl Client {
let mut contents = String::with_capacity(entry.size() as usize);
entry.read_to_string(&mut contents)?;
group.register(InputKind::Action, contents, key, options.strict)?;
} else if matches!(
file_path.file_name(),
Some("dependabot.yml" | "dependabot.yaml")
) {
let key = InputKey::remote(slug, file_path.to_string())?;
let mut contents = String::with_capacity(entry.size() as usize);
entry.read_to_string(&mut contents)?;
group.register(InputKind::Dependabot, contents, key, options.strict)?;
}
}

View file

@ -364,6 +364,8 @@ pub(crate) enum CollectionMode {
WorkflowsOnly,
/// Collect only action definitions (i.e. `action.yml`).
ActionsOnly,
/// Collect only Dependabot configuration files (i.e. `dependabot.yml`).
DependabotOnly,
}
impl CollectionMode {
@ -387,6 +389,13 @@ impl CollectionMode {
CollectionMode::All | CollectionMode::Default | CollectionMode::ActionsOnly
)
}
pub(crate) fn dependabot(&self) -> bool {
matches!(
self,
CollectionMode::All | CollectionMode::Default | CollectionMode::DependabotOnly
)
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]

View file

@ -11,6 +11,7 @@ use crate::models::inputs::HasInputs;
pub(crate) mod action;
pub(crate) mod coordinate;
pub(crate) mod dependabot;
pub(crate) mod inputs;
pub(crate) mod uses;
pub(crate) mod workflow;

View file

@ -0,0 +1,79 @@
//! Dependabot config models.
//!
//! These models enrich the models under [`github_actions_models::dependabot`],
//! providing higher-level APIs for zizmor to use.
use github_actions_models::dependabot;
use terminal_link::Link;
use crate::{
finding::location::{SymbolicFeature, SymbolicLocation},
models::AsDocument,
registry::input::{CollectionError, InputKey},
utils::{DEPENDABOT_VALIDATOR, from_str_with_validation},
};
pub(crate) struct Dependabot {
pub(crate) key: InputKey,
pub(crate) link: Option<String>,
document: yamlpath::Document,
inner: dependabot::v2::Dependabot,
}
impl<'a> AsDocument<'a, 'a> for Dependabot {
fn as_document(&'a self) -> &'a yamlpath::Document {
&self.document
}
}
impl std::ops::Deref for Dependabot {
type Target = dependabot::v2::Dependabot;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::fmt::Debug for Dependabot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{key}", key = self.key)
}
}
impl Dependabot {
pub(crate) fn from_string(contents: String, key: InputKey) -> Result<Self, CollectionError> {
let inner = from_str_with_validation(&contents, &DEPENDABOT_VALIDATOR)?;
let document = yamlpath::Document::new(&contents)?;
let link = match key {
InputKey::Local(_) => None,
InputKey::Remote(_) => {
// NOTE: InputKey's Display produces a URL, hence `key.to_string()`.
Some(Link::new(key.presentation_path(), &key.to_string()).to_string())
}
};
Ok(Self {
key,
link,
document,
inner,
})
}
/// Returns this Dependabot config's [`SymbolicLocation`].
///
/// See [`Workflow::location`] for an explanation of why this isn't
/// implemented through the [`Locatable`] trait.
pub(crate) fn location(&self) -> SymbolicLocation<'_> {
SymbolicLocation {
key: &self.key,
annotation: "this config".to_string(),
link: None,
route: Default::default(),
feature_kind: SymbolicFeature::Normal,
kind: Default::default(),
}
}
}

View file

@ -15,7 +15,7 @@ use crate::{
audit::AuditInput,
config::{Config, ConfigError},
github_api::{Client, ClientError},
models::{action::Action, workflow::Workflow},
models::{action::Action, dependabot::Dependabot, workflow::Workflow},
};
/// Errors that can occur while collecting inputs.
@ -99,6 +99,8 @@ pub(crate) enum InputKind {
Workflow,
/// An action definition.
Action,
/// A Dependabot configuration file.
Dependabot,
}
impl std::fmt::Display for InputKind {
@ -106,6 +108,7 @@ impl std::fmt::Display for InputKind {
match self {
InputKind::Workflow => write!(f, "workflow"),
InputKind::Action => write!(f, "action"),
InputKind::Dependabot => write!(f, "dependabot config"),
}
}
}
@ -348,6 +351,9 @@ impl InputGroup {
let input: Result<AuditInput, CollectionError> = match kind {
InputKind::Workflow => Workflow::from_string(contents, key.clone()).map(|wf| wf.into()),
InputKind::Action => Action::from_string(contents, key.clone()).map(|a| a.into()),
InputKind::Dependabot => {
Dependabot::from_string(contents, key.clone()).map(|d| d.into())
}
};
match input {
@ -375,6 +381,11 @@ impl InputGroup {
// 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()) {
(Some("dependabot"), Some("yml" | "yaml")) => (
// NOTE: Safe unwrap because we just checked the filename.
InputKey::local(Group(path.as_str().into()), path, None).unwrap(),
InputKind::Dependabot,
),
(Some("action"), Some("yml" | "yaml")) => (
// NOTE: Safe unwrap because we just checked the filename.
InputKey::local(Group(path.as_str().into()), path, None).unwrap(),
@ -466,6 +477,25 @@ impl InputGroup {
})?;
group.register(InputKind::Action, contents, key, options.strict)?;
}
if options.mode.dependabot()
&& entry.is_file()
&& matches!(
entry.file_name(),
Some("dependabot.yml" | "dependabot.yaml")
)
{
// NOTE: Safe unwrap because we just checked the filename.
let key = InputKey::local(Group(path.as_str().into()), entry, Some(path)).unwrap();
let contents = std::fs::read_to_string(entry).map_err(|e| {
CollectionError::Inner(
CollectionError::Io(e).into(),
key.to_string(),
InputKind::Dependabot,
)
})?;
group.register(InputKind::Dependabot, contents, key, options.strict)?;
}
}
Ok(group)

View file

@ -18,13 +18,18 @@ use std::{fmt::Write, sync::LazyLock};
use crate::{audit::AuditInput, models::AsDocument, registry::input::CollectionError};
pub(crate) static WORKFLOW_VALIDATOR: LazyLock<Validator> = LazyLock::new(|| {
validator_for(&serde_json::from_str(include_str!("./data/github-workflow.json")).unwrap())
.unwrap()
});
pub(crate) static ACTION_VALIDATOR: LazyLock<Validator> = LazyLock::new(|| {
validator_for(&serde_json::from_str(include_str!("./data/github-action.json")).unwrap())
.unwrap()
});
pub(crate) static WORKFLOW_VALIDATOR: LazyLock<Validator> = LazyLock::new(|| {
validator_for(&serde_json::from_str(include_str!("./data/github-workflow.json")).unwrap())
pub(crate) static DEPENDABOT_VALIDATOR: LazyLock<Validator> = LazyLock::new(|| {
validator_for(&serde_json::from_str(include_str!("./data/dependabot-2.0.json")).unwrap())
.unwrap()
});

View file

@ -43,7 +43,7 @@ Options:
--cache-dir <CACHE_DIR>
The directory to use for HTTP caching. By default, a host-appropriate user-caching directory will be used
--collect <COLLECT>
Control which kinds of inputs are collected for auditing [default: default] [possible values: all, default, workflows-only, actions-only]
Control which kinds of inputs are collected for auditing [default: default] [possible values: all, default, workflows-only, actions-only, dependabot-only]
--strict-collection
Fail instead of warning on syntax and schema errors in collected inputs
--completions <SHELL>