diff --git a/src/audit/hardcoded_container_credentials.rs b/src/audit/hardcoded_container_credentials.rs new file mode 100644 index 00000000..3036adc5 --- /dev/null +++ b/src/audit/hardcoded_container_credentials.rs @@ -0,0 +1,101 @@ +use std::ops::Deref; + +use github_actions_models::{ + common::Expression, + workflow::{ + job::{Container, DockerCredentials}, + Job, + }, +}; + +use crate::{ + finding::{Confidence, Severity}, + models::AuditConfig, +}; + +use super::WorkflowAudit; + +pub(crate) struct HardcodedContainerCredentials<'a> { + pub(crate) _config: AuditConfig<'a>, +} + +impl<'a> WorkflowAudit<'a> for HardcodedContainerCredentials<'a> { + fn ident() -> &'static str + where + Self: Sized, + { + "hardcoded-container-credentials" + } + + fn new(config: crate::models::AuditConfig<'a>) -> anyhow::Result + where + Self: Sized, + { + Ok(Self { _config: config }) + } + + fn audit<'w>( + &self, + workflow: &'w crate::models::Workflow, + ) -> anyhow::Result>> { + let mut findings = vec![]; + + for job in workflow.jobs() { + let Job::NormalJob(normal) = job.deref() else { + continue; + }; + + if let Some(Container::Container { + image: _, + credentials: + Some(DockerCredentials { + username: _, + password: Some(password), + }), + .. + }) = &normal.container + { + // If the password doesn't parse as an expression, it's hardcoded. + if Expression::from_curly(password.into()).is_none() { + findings.push( + Self::finding() + .severity(Severity::High) + .confidence(Confidence::High) + .add_location( + job.location() + .annotated("container registry password is hard-coded"), + ) + .build(workflow)?, + ) + } + } + + for (service, config) in normal.services.iter() { + if let Container::Container { + image: _, + credentials: + Some(DockerCredentials { + username: _, + password: Some(password), + }), + .. + } = &config + { + if Expression::from_curly(password.into()).is_none() { + findings.push( + Self::finding() + .severity(Severity::High) + .confidence(Confidence::High) + .add_location(job.location().annotated(format!( + "service {service}: container registry password is hard-coded" + ))) + .build(workflow)?, + ) + } + } + } + } + + Ok(findings) + } +} diff --git a/src/audit/mod.rs b/src/audit/mod.rs index 669be29b..e8f5da43 100644 --- a/src/audit/mod.rs +++ b/src/audit/mod.rs @@ -5,6 +5,7 @@ use crate::{ use anyhow::Result; pub(crate) mod artipacked; +pub(crate) mod hardcoded_container_credentials; pub(crate) mod impostor_commit; pub(crate) mod pull_request_target; pub(crate) mod ref_confusion; diff --git a/src/main.rs b/src/main.rs index 5155ca61..984a4653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,6 +86,7 @@ fn main() -> Result<()> { &audit::ref_confusion::RefConfusion::new(config)?, &audit::use_trusted_publishing::UseTrustedPublishing::new(config)?, &audit::template_injection::TemplateInjection::new(config)?, + &audit::hardcoded_container_credentials::HardcodedContainerCredentials::new(config)?, ]; for workflow in workflows.iter() { // TODO: Proper abstraction for multiple audits here.