use super::check_name::CheckName;
use super::report::Finding;
use super::target::AuditTarget;
use crate::config::Level;
use crate::infra::github::RepoMetadata;
const ARCHIVED_SEVERITY: Level = Level::Warn;
const LOOKUP_FAILURE_SEVERITY: Level = Level::Error;
pub fn archived_action(target: &AuditTarget<'_>, source: &dyn RepoMetadata) -> Option<Finding> {
let repo = target.id.base_repo();
let meta = match source.metadata(repo.as_str()) {
Ok(meta) => meta,
Err(reason) => {
return Some(Finding::new(
CheckName::ArchivedAction,
LOOKUP_FAILURE_SEVERITY,
format!(
"could not determine whether {} is archived: {reason}",
target.id
),
));
}
};
if !meta.archived {
return None;
}
Some(Finding::new(
CheckName::ArchivedAction,
ARCHIVED_SEVERITY,
format!(
"{} is published from an archived repository (last pushed {}); \
it will receive no security fixes while archived, so migrate to a maintained alternative",
target.id,
day_of(&meta.pushed_at),
),
))
}
fn day_of(timestamp: &str) -> &str {
timestamp.split_once('T').map_or(timestamp, |(day, _)| day)
}
#[cfg(test)]
#[expect(
clippy::expect_used,
reason = "tests use unwrap, indexing, and other patterns freely"
)]
mod tests {
use super::{AuditTarget, CheckName, Level, archived_action, day_of};
use crate::domain::action::identity::{ActionId, CommitSha};
use crate::infra::github::CannedRepos;
const SHA: &str = "abc123def456789012345678901234567890abcd";
fn target<'lock>(id: &'lock ActionId, sha: &'lock CommitSha) -> AuditTarget<'lock> {
AuditTarget {
id,
version: "v1",
sha,
ref_type: None,
repository: None,
}
}
#[test]
fn archived_repository_is_reported_as_a_warning() {
let id = ActionId::from("actions/setup-ruby");
let sha = CommitSha::from(SHA);
let source = CannedRepos::archived("2021-04-14T18:22:31Z");
let finding =
archived_action(&target(&id, &sha), &source).expect("archived repo must be reported");
assert_eq!(finding.rule, CheckName::ArchivedAction);
assert_eq!(finding.level, Level::Warn);
assert!(
finding.message.contains("actions/setup-ruby"),
"must name the action: {}",
finding.message
);
assert!(
finding.message.contains("2021-04-14"),
"must state how stale the dependency is: {}",
finding.message
);
assert!(
finding.message.contains("migrate"),
"must point at migration, not upgrade: {}",
finding.message
);
assert!(
finding.message.contains("while archived"),
"must not imply that a repository cannot be unarchived: {}",
finding.message
);
assert!(
!finding.message.contains("18:22:31"),
"time of day is noise at this granularity: {}",
finding.message
);
}
#[test]
fn active_repository_is_not_reported_but_is_still_looked_up() {
let id = ActionId::from("actions/checkout");
let sha = CommitSha::from(SHA);
let source = CannedRepos::active();
assert!(archived_action(&target(&id, &sha), &source).is_none());
assert_eq!(source.seen.borrow().as_slice(), ["actions/checkout"]);
}
#[test]
fn subpath_action_is_looked_up_by_its_base_repository() {
let id = ActionId::from("github/codeql-action/upload-sarif");
let sha = CommitSha::from(SHA);
let source = CannedRepos::active();
archived_action(&target(&id, &sha), &source);
assert_eq!(source.seen.borrow().as_slice(), ["github/codeql-action"]);
}
#[test]
fn subpath_action_is_reported_under_its_full_name() {
let id = ActionId::from("github/codeql-action/upload-sarif");
let sha = CommitSha::from(SHA);
let source = CannedRepos::archived("2021-04-14T18:22:31Z");
let finding =
archived_action(&target(&id, &sha), &source).expect("archived repo must be reported");
assert!(
finding
.message
.contains("github/codeql-action/upload-sarif"),
"must name the action including its subpath: {}",
finding.message
);
}
#[test]
fn failed_lookup_is_an_error_naming_the_action_and_the_reason() {
let id = ActionId::from("actions/checkout");
let sha = CommitSha::from(SHA);
let source = CannedRepos::failing();
let finding = archived_action(&target(&id, &sha), &source)
.expect("a failed lookup must not read as clean");
assert_eq!(finding.rule, CheckName::ArchivedAction);
assert_eq!(finding.level, Level::Error);
assert!(
finding.message.contains("could not determine"),
"must lead with the uncertainty, not a verdict: {}",
finding.message
);
assert!(
finding.message.contains("actions/checkout"),
"must name the action: {}",
finding.message
);
assert!(
finding.message.contains("not found"),
"must carry the underlying reason: {}",
finding.message
);
}
#[test]
fn a_timestamp_without_a_time_is_shown_whole() {
assert_eq!(day_of("2021-04-14"), "2021-04-14");
assert_eq!(day_of("2021-04-14T18:22:31Z"), "2021-04-14");
assert_eq!(day_of(""), "");
}
}