use std::ops::Deref;
use github_actions_expressions::{
Expr, SpannedExpr,
call::{Call, Function},
context::Context,
};
use crate::{
Confidence, Severity,
audit::AuditError,
finding::location::{Feature, Location},
utils::parse_fenced_expressions_from_routable,
};
use super::{Audit, AuditLoadError, AuditState, audit_meta};
pub(crate) struct UnredactedSecrets;
audit_meta!(
UnredactedSecrets,
"unredacted-secrets",
"leaked secret values"
);
#[async_trait::async_trait]
impl Audit for UnredactedSecrets {
fn new(_state: &AuditState) -> Result<Self, AuditLoadError>
where
Self: Sized,
{
Ok(Self)
}
async fn audit_raw<'doc>(
&self,
input: &'doc super::AuditInput,
_config: &crate::config::Config,
) -> Result<Vec<crate::finding::Finding<'doc>>, AuditError> {
let mut findings = vec![];
for (expr, span) in parse_fenced_expressions_from_routable(input) {
let Ok(parsed) = Expr::parse(expr.as_bare()) else {
tracing::warn!("couldn't parse expression: {expr}", expr = expr.as_bare());
continue;
};
for _ in Self::secret_leakages(&parsed) {
findings.push(
Self::finding()
.confidence(Confidence::High)
.severity(Severity::Medium)
.add_raw_location(Location::new(
input
.location()
.annotated("bypasses secret redaction")
.primary(),
Feature::from_span(&span, input),
))
.build(input)?,
);
}
}
findings.len();
Ok(findings)
}
}
impl UnredactedSecrets {
fn secret_leakages(expr: &SpannedExpr) -> Vec<()> {
let mut results = vec![];
match expr.deref() {
Expr::Call(Call { func, args }) => {
if matches!(func, Function::FromJSON)
&& args.iter().any(
|arg| matches!(arg.deref(), Expr::Context(ctx) if ctx.child_of("secrets")),
)
{
results.push(());
} else {
results.extend(args.iter().flat_map(Self::secret_leakages));
}
}
Expr::Index(expr) => results.extend(Self::secret_leakages(expr)),
Expr::Context(Context { parts, .. }) => {
results.extend(parts.iter().flat_map(Self::secret_leakages))
}
Expr::BinOp { lhs, op: _, rhs } => {
results.extend(Self::secret_leakages(lhs));
results.extend(Self::secret_leakages(rhs));
}
Expr::UnOp { op: _, expr } => results.extend(Self::secret_leakages(expr)),
_ => (),
}
results
}
}
#[cfg(test)]
mod tests {
use github_actions_expressions::Expr;
use crate::audit::unredacted_secrets;
#[test]
fn test_secret_leakages() {
for (expr, count) in &[
("secrets", 0),
("secrets.foo", 0),
("fromJSON(notsecrets)", 0),
("fromJSON(notsecrets.secrets)", 0),
("fromJSON(secrets)", 1),
("fromjson(SECRETS)", 1),
("fromJSON(secrets.foo)", 1),
("fromJSON(secrets.foo).bar", 1),
("fromJSON(secrets.foo).bar.baz", 1),
("fromJSON(secrets.foo) && fromJSON(secrets.bar)", 2),
] {
let expr = Expr::parse(expr).unwrap();
assert_eq!(
unredacted_secrets::UnredactedSecrets::secret_leakages(&expr).len(),
*count
);
}
}
}