use std::path::Path;
use runner_manager_domain::store::SqliteStore;
use crate::support::{fixture_device_code, fixture_token, is_the_secret_store};
use super::scenario::{Invocation, Scenario};
#[must_use]
pub fn explicit_canary() -> String {
format!("{}{}", "cli-chains-protected-", "canary-71d3e9b4")
}
#[must_use]
pub fn protected_values() -> Vec<(&'static str, String)> {
vec![
("fixture token", fixture_token()),
("fixture device code", fixture_device_code()),
(
"fixture JIT configuration",
runner_manager_testkit::github::DEFAULT_JIT_CONFIG.to_string(),
),
("explicit canary", explicit_canary()),
]
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum SecurityPlane {
Stdout,
Stderr,
Logs,
DataTree,
SqliteDump,
}
impl SecurityPlane {
pub const ALL: [Self; 5] = [
Self::Stdout,
Self::Stderr,
Self::Logs,
Self::DataTree,
Self::SqliteDump,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Stdout => "stdout",
Self::Stderr => "stderr",
Self::Logs => "logs",
Self::DataTree => "temporary data tree",
Self::SqliteDump => "SQLite textual dump",
}
}
}
#[derive(Debug, Clone)]
pub struct Fragment {
pub plane: SecurityPlane,
pub origin: String,
pub text: String,
}
#[must_use]
pub fn findings(fragments: &[Fragment]) -> Vec<String> {
let mut found = Vec::new();
for (name, needle) in protected_values() {
for fragment in fragments {
if fragment.text.contains(&needle) {
found.push(format!(
"{name} appears in {} ({})",
fragment.origin,
fragment.plane.name()
));
}
}
}
found
}
pub fn collect(
scenario: &Scenario,
invocation: Option<&Invocation>,
) -> Result<Vec<Fragment>, String> {
let mut fragments = Vec::new();
if let Some(invocation) = invocation {
fragments.push(Fragment {
plane: SecurityPlane::Stdout,
origin: "the action's stdout".to_string(),
text: invocation.stdout.clone(),
});
fragments.push(Fragment {
plane: SecurityPlane::Stderr,
origin: "the action's stderr".to_string(),
text: invocation.stderr.clone(),
});
}
let protected = protected_values();
for path in scannable_files(&scenario.data)? {
let relative = path.strip_prefix(&scenario.data).unwrap_or(&path);
let plane = if relative.starts_with(Path::new("logs")) {
SecurityPlane::Logs
} else {
SecurityPlane::DataTree
};
let bytes = std::fs::read(&path)
.map_err(|error| format!("cannot scan {}: {error}", path.display()))?;
for (name, needle) in &protected {
if bytes
.windows(needle.len())
.any(|window| window == needle.as_bytes())
{
fragments.push(Fragment {
plane,
origin: path.display().to_string(),
text: format!("{name}:{needle}"),
});
}
}
}
if let Some(store) = scenario.store()? {
fragments.push(Fragment {
plane: SecurityPlane::SqliteDump,
origin: "SqliteStore::dump_text()".to_string(),
text: dump(&store)?,
});
}
Ok(fragments)
}
fn scannable_files(root: &Path) -> Result<Vec<std::path::PathBuf>, String> {
if !root.exists() {
return Ok(Vec::new());
}
let mut files = Vec::new();
let mut pending = vec![root.to_path_buf()];
while let Some(directory) = pending.pop() {
let entries = std::fs::read_dir(&directory)
.map_err(|error| format!("cannot enumerate {}: {error}", directory.display()))?;
for entry in entries {
let entry = entry
.map_err(|error| format!("cannot enumerate {}: {error}", directory.display()))?;
let path = entry.path();
if is_the_secret_store(&path) {
continue;
}
let kind = entry
.file_type()
.map_err(|error| format!("cannot inspect {}: {error}", path.display()))?;
if kind.is_dir() {
pending.push(path);
} else {
files.push(path);
}
}
}
Ok(files)
}
fn dump(store: &SqliteStore) -> Result<String, String> {
store
.dump_text()
.map_err(|error| format!("cannot dump SQLite for the security scan: {error}"))
}