add use_trusted_publishing audit

Signed-off-by: William Woodruff <william@yossarian.net>
This commit is contained in:
William Woodruff 2024-08-22 17:35:18 -04:00
parent 72187f3c70
commit 7a6ddfd304
No known key found for this signature in database
4 changed files with 133 additions and 3 deletions

View file

@ -85,7 +85,7 @@ impl<'a> WorkflowAudit<'a> for Artipacked<'a> {
severity: Severity::Medium,
confidence: Confidence::Low,
},
locations: vec![checkout.location().clone()],
locations: vec![checkout.location()],
})
}
} else {
@ -103,7 +103,7 @@ impl<'a> WorkflowAudit<'a> for Artipacked<'a> {
severity: Severity::High,
confidence: Confidence::High,
},
locations: vec![checkout.location().clone(), upload.location().clone()],
locations: vec![checkout.location(), upload.location()],
});
}
}

View file

@ -7,6 +7,7 @@ use anyhow::Result;
pub(crate) mod artipacked;
pub(crate) mod impostor_commit;
pub(crate) mod pull_request_target;
pub(crate) mod use_trusted_publishing;
pub(crate) trait WorkflowAudit<'a> {
fn ident() -> &'static str

View file

@ -0,0 +1,128 @@
use std::collections::HashMap;
use github_actions_models::{
common::EnvValue,
workflow::{job::StepBody, Job},
};
use crate::{
finding::{Confidence, Determinations, Finding, Severity},
models::AuditConfig,
};
use super::WorkflowAudit;
const KNOWN_PYTHON_TP_INDICES: &'static [&'static str] = &[
"https://upload.pypi.org/legacy/",
"https://test.pypi.org/legacy/",
];
pub(crate) struct UseTrustedPublishing<'a> {
pub(crate) _config: AuditConfig<'a>,
}
impl<'a> UseTrustedPublishing<'a> {
fn pypi_publish_uses_manual_credentials(&self, with: &HashMap<String, EnvValue>) -> bool {
// `password` implies the step isn't using Trusted Publishing,
// but we also need to check `repository-url` to prevent false-positives
// on third-party indices.
let has_manual_credential = with.contains_key("password");
match with
.get("repository-url")
.or_else(|| with.get("repository_url"))
{
Some(repo_url) => {
has_manual_credential
&& KNOWN_PYTHON_TP_INDICES.contains(&repo_url.to_string().as_str())
}
None => has_manual_credential,
}
}
fn release_gem_uses_manual_credentials(&self, with: &HashMap<String, EnvValue>) -> bool {
match with.get("setup-trusted-publisher") {
Some(v) if v.to_string() == "true" => false,
// Anything besides `true` means to *not* use trusted publishing.
Some(_) => true,
// Not set means the default, which is trusted publishing.
None => false,
}
}
fn rubygems_credential_uses_manual_credentials(
&self,
with: &HashMap<String, EnvValue>,
) -> bool {
with.contains_key("api-token")
}
}
impl<'a> WorkflowAudit<'a> for UseTrustedPublishing<'a> {
fn ident() -> &'static str {
"use-trusted-publishing"
}
fn new(config: AuditConfig<'a>) -> anyhow::Result<Self> {
Ok(Self { _config: config })
}
fn audit<'w>(
&self,
workflow: &'w crate::models::Workflow,
) -> anyhow::Result<Vec<crate::finding::Finding<'w>>> {
log::debug!("audit: {} evaluating {}", Self::ident(), &workflow.filename);
let mut findings = vec![];
for job in workflow.jobs() {
if !matches!(job.inner, Job::NormalJob(_)) {
continue;
}
for step in job.steps() {
let StepBody::Uses { uses, with } = &step.inner.body else {
continue;
};
if uses.starts_with("pypa/gh-action-pypi-publish") {
if self.pypi_publish_uses_manual_credentials(&with) {
findings.push(Finding {
ident: UseTrustedPublishing::ident(),
determinations: Determinations {
severity: Severity::Informational,
confidence: Confidence::High,
},
locations: vec![step.location()],
})
}
} else if uses.starts_with("rubygems/release-gem") {
if self.release_gem_uses_manual_credentials(&with) {
findings.push(Finding {
ident: UseTrustedPublishing::ident(),
determinations: Determinations {
confidence: Confidence::High,
severity: Severity::Informational,
},
locations: vec![step.location()],
})
}
} else if uses.starts_with("rubygems/configure-rubygems-credential") {
if self.rubygems_credential_uses_manual_credentials(&with) {
findings.push(Finding {
ident: UseTrustedPublishing::ident(),
determinations: Determinations {
confidence: Confidence::High,
severity: Severity::Informational,
},
locations: vec![step.location()],
})
}
}
}
}
log::debug!("audit: {} completed {}", Self::ident(), &workflow.filename);
Ok(findings)
}
}

View file

@ -74,10 +74,11 @@ fn main() -> Result<()> {
}
let mut results = vec![];
let audits: [&dyn WorkflowAudit; 3] = [
let audits: &[&dyn WorkflowAudit] = &[
&audit::artipacked::Artipacked::new(config)?,
&audit::pull_request_target::PullRequestTarget::new(config)?,
&audit::impostor_commit::ImpostorCommit::new(config)?,
&audit::use_trusted_publishing::UseTrustedPublishing::new(config)?,
];
for workflow in workflows.iter() {
// TODO: Proper abstraction for multiple audits here.