feat: dependabot-execution audit (#1220)

This commit is contained in:
William Woodruff 2025-10-07 18:24:48 -04:00 committed by GitHub
parent fbd86b5955
commit 62655cb7c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 224 additions and 6 deletions

View file

@ -0,0 +1,52 @@
use github_actions_models::dependabot::v2::AllowDeny;
use crate::{
audit::{Audit, audit_meta},
finding::location::Locatable as _,
};
audit_meta!(
DependabotExecution,
"dependabot-execution",
"external code execution in Dependabot updates"
);
pub(crate) struct DependabotExecution;
impl Audit for DependabotExecution {
fn new(_state: &crate::state::AuditState) -> Result<Self, super::AuditLoadError>
where
Self: Sized,
{
Ok(Self)
}
fn audit_dependabot<'doc>(
&self,
dependabot: &'doc crate::models::dependabot::Dependabot,
_config: &crate::config::Config,
) -> anyhow::Result<Vec<crate::finding::Finding<'doc>>> {
let mut findings = vec![];
for update in dependabot.updates() {
if matches!(update.insecure_external_code_execution, AllowDeny::Allow) {
findings.push(
Self::finding()
.confidence(crate::finding::Confidence::High)
.severity(crate::finding::Severity::High)
.add_location(
update
.location()
.with_keys(["insecure-external-code-execution".into()])
.primary()
.annotated("enabled here"),
)
.add_location(update.location_with_name())
.build(dependabot)?,
);
}
}
Ok(findings)
}
}

View file

@ -22,6 +22,7 @@ pub(crate) mod artipacked;
pub(crate) mod bot_conditions;
pub(crate) mod cache_poisoning;
pub(crate) mod dangerous_triggers;
pub(crate) mod dependabot_execution;
pub(crate) mod excessive_permissions;
pub(crate) mod forbidden_uses;
pub(crate) mod github_env;

View file

@ -7,7 +7,7 @@ use github_actions_models::dependabot;
use terminal_link::Link;
use crate::{
finding::location::{SymbolicFeature, SymbolicLocation},
finding::location::{Locatable, SymbolicFeature, SymbolicLocation},
models::AsDocument,
registry::input::{CollectionError, InputKey},
utils::{DEPENDABOT_VALIDATOR, from_str_with_validation},
@ -76,4 +76,64 @@ impl Dependabot {
kind: Default::default(),
}
}
pub(crate) fn updates(&self) -> Updates<'_> {
Updates::new(self)
}
}
pub(crate) struct Updates<'doc> {
parent: &'doc Dependabot,
inner:
std::iter::Enumerate<std::slice::Iter<'doc, github_actions_models::dependabot::v2::Update>>,
}
impl<'doc> Updates<'doc> {
fn new(parent: &'doc Dependabot) -> Self {
Self {
parent,
inner: parent.inner.updates.iter().enumerate(),
}
}
}
impl<'doc> Iterator for Updates<'doc> {
type Item = Update<'doc>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(idx, update)| Update {
parent: self.parent,
index: idx,
inner: update,
})
}
}
pub(crate) struct Update<'doc> {
parent: &'doc Dependabot,
index: usize,
inner: &'doc github_actions_models::dependabot::v2::Update,
}
impl<'doc> std::ops::Deref for Update<'doc> {
type Target = github_actions_models::dependabot::v2::Update;
fn deref(&self) -> &Self::Target {
self.inner
}
}
impl<'doc> Locatable<'doc> for Update<'doc> {
fn location(&self) -> SymbolicLocation<'doc> {
self.parent
.location()
.with_keys(["updates".into(), self.index.into()])
.annotated("this update rule")
}
fn location_with_name(&self) -> SymbolicLocation<'doc> {
self.location()
.with_keys(["package-ecosystem".into()])
.annotated("this ecosystem")
}
}

View file

@ -78,6 +78,7 @@ impl AuditRegistry {
register_audit!(audit::anonymous_definition::AnonymousDefinition);
register_audit!(audit::unsound_condition::UnsoundCondition);
register_audit!(audit::ref_version_mismatch::RefVersionMismatch);
register_audit!(audit::dependabot_execution::DependabotExecution);
Ok(registry)
}

View file

@ -948,3 +948,16 @@ fn unsound_condition() -> Result<()> {
Ok(())
}
#[test]
fn dependabot_execution() -> Result<()> {
insta::assert_snapshot!(
zizmor()
.input(input_under_test(
"dependabot-execution/basic/dependabot.yml"
))
.run()?
);
Ok(())
}

View file

@ -0,0 +1,16 @@
---
source: crates/zizmor/tests/integration/snapshot.rs
expression: "zizmor().input(input_under_test(\"dependabot-execution/basic/dependabot.yml\")).run()?"
---
error[dependabot-execution]: external code execution in Dependabot updates
--> @@INPUT@@:8:5
|
4 | - package-ecosystem: pip
| ---------------------- this ecosystem
...
8 | insecure-external-code-execution: allow
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ enabled here
|
= note: audit confidence → High
1 finding: 0 informational, 0 low, 0 medium, 1 high

View file

@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: pip
directory: /
schedule:
interval: daily
insecure-external-code-execution: allow

View file

@ -327,6 +327,71 @@ Some general pointers:
[reusable workflow]: https://docs.github.com/en/actions/sharing-automations/reusing-workflows
## `dependabot-execution`
| Type | Examples | Introduced in | Works offline | Enabled by default | Configurable |
|----------|-------------------------|---------------|----------------|--------------------| ---------------|
| Dependabot | [dependabot.yml] | v1.15.0 | ✅ | ✅ | ❌ |
[dependabot.yml]: https://github.com/zizmorcore/zizmor/blob/main/crates/zizmor/tests/integration/test-data/dependabot-execution/basic/dependabot.yml
Detects usages of `insecure-external-code-execution` in Dependabot configuration
files.
By default, Dependabot does not execution code from dependency manifests
during updates. However, users can opt in to this behavior by setting
`#!yaml insecure-external-code-execution: allow` in their Dependabot
configuration.
Some ecosystems (including but not limited to Python, Ruby, and JavaScript)
depend partially on code execution during dependency resolution.
In these ecosystems fully avoiding build-time code execution is impossible.
However, build-time code execution *should* be avoided in automated dependency
update contexts like Dependabot, since a compromised dependency may be able
to obtain credentials or private source access automatically through
a Dependabot job.
Other resources:
* [`insecure-external-code-execution` documentation](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#insecure-external-code-execution--)
* [Dependabot: Allowing external code execution](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/configuring-access-to-private-registries-for-dependabot#allowing-external-code-execution)
### Remediation
In general, automatic dependency updates should be limited to only updates
that do not require code execution at resolution time.
In practice, this means that users should set
`#!yaml insecure-external-code-execution: deny` **or** omit the field entirely
(and rely on the default of `deny`).
!!! example
=== "Before :warning:"
```yaml title="dependabot.yml" hl_lines="7"
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
insecure-external-code-execution: allow
```
=== "After :white_check_mark:"
```yaml title="dependabot.yml" hl_lines="7"
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
insecure-external-code-execution: deny
```
## `excessive-permissions`
| Type | Examples | Introduced in | Works offline | Enabled by default | Configurable |

View file

@ -49,9 +49,8 @@ sources can be mixed and matched:
zizmor ../example.yml ../other-repo/ example/example
```
When auditing local and/or remote repositories, `zizmor` will collect both
workflows (e.g. `.github/workflows/ci.yml`) **and** action definitions
(e.g. `custom-action/foo.yml`) by default. To configure collection behavior,
When auditing local and/or remote repositories, `zizmor` will collect
all known input kinds by default. To configure collection behavior,
you can use the `--collect=...` option.
```bash
@ -66,6 +65,9 @@ zizmor --collect=workflows-only example/example
# collect only actions
zizmor --collect=actions-only example/example
# collect only Dependabot configs
zizmor --collect=dependabot-only example/example
```
!!! tip
@ -81,8 +83,8 @@ zizmor --collect=actions-only example/example
*will* audit `workflow.yml`, since it was passed explicitly and not
collected indirectly.
By default, `zizmor` will warn (but not fail) if it fails to parse a
workflow or action definition. To turn these warnings into failures,
By default, `zizmor` will warn (but not fail) if it fails to parse an
input file. To turn these warnings into failures,
you can use the `--strict-collection` option:
```bash