use std::path::PathBuf;
use std::process::ExitCode;
#[cfg(any(feature = "deps", feature = "github"))]
use anyhow::Context;
use anyhow::Result;
use clap::Subcommand;
#[cfg(any(feature = "deps", feature = "github"))]
use secunit_capture::canonical::Envelope;
#[derive(Debug, Subcommand)]
pub enum CaptureCmd {
Deps {
#[command(subcommand)]
sub: DepsCmd,
},
Github {
#[command(subcommand)]
sub: GithubCmd,
},
}
#[derive(Debug, Subcommand)]
pub enum DepsCmd {
PipAudit {
#[arg(long)]
path: PathBuf,
#[arg(long)]
out: PathBuf,
},
PnpmAudit {
#[arg(long)]
path: PathBuf,
#[arg(long)]
out: PathBuf,
},
CargoAudit {
#[arg(long)]
path: PathBuf,
#[arg(long)]
db_path: Option<PathBuf>,
#[arg(long)]
out: PathBuf,
},
OsvQuery {
#[arg(long)]
ecosystem: String,
#[arg(long)]
package: String,
#[arg(long)]
version: String,
#[arg(long)]
out: PathBuf,
},
}
#[derive(Debug, Subcommand)]
pub enum GithubCmd {
DependabotAlerts {
#[arg(long)]
repo: String,
#[arg(long)]
state: Option<String>,
#[arg(long)]
out: PathBuf,
},
BranchProtection {
#[arg(long)]
repo: String,
#[arg(long)]
branch: String,
#[arg(long)]
out: PathBuf,
},
OrgMembers {
#[arg(long)]
org: String,
#[arg(long)]
out: PathBuf,
},
AuditLog {
#[arg(long)]
org: String,
#[arg(long, value_name = "ISO_8601")]
since: String,
#[arg(long)]
out: PathBuf,
},
CodeqlAlerts {
#[arg(long)]
repo: String,
#[arg(long)]
out: PathBuf,
},
}
pub fn run(cmd: CaptureCmd) -> Result<ExitCode> {
match cmd {
CaptureCmd::Deps { sub } => deps(sub),
CaptureCmd::Github { sub } => github(sub),
}
}
#[cfg(feature = "deps")]
fn deps(cmd: DepsCmd) -> Result<ExitCode> {
let env = match cmd {
DepsCmd::PipAudit { path, out } => {
let env = secunit_capture::deps::pip_audit::capture(&path).map_err(map_runtime)?;
(env, out)
}
DepsCmd::PnpmAudit { path, out } => {
let env = secunit_capture::deps::pnpm_audit::capture(&path).map_err(map_runtime)?;
(env, out)
}
DepsCmd::CargoAudit { path, db_path, out } => {
let env = secunit_capture::deps::cargo_audit::capture(&path, db_path.as_deref())
.map_err(map_runtime)?;
(env, out)
}
DepsCmd::OsvQuery {
ecosystem,
package,
version,
out,
} => {
let rt = build_runtime()?;
let env = rt
.block_on(secunit_capture::deps::osv_query::capture(
secunit_capture::deps::osv_query::OsvArgs {
ecosystem: &ecosystem,
package: &package,
version: &version,
},
))
.map_err(map_runtime)?;
(env, out)
}
};
write_envelope(&env.0, &env.1)?;
Ok(ExitCode::SUCCESS)
}
#[cfg(not(feature = "deps"))]
fn deps(_cmd: DepsCmd) -> Result<ExitCode> {
eprintln!("error: this binary was built without the `deps` feature");
Ok(ExitCode::from(2))
}
#[cfg(feature = "github")]
fn github(cmd: GithubCmd) -> Result<ExitCode> {
use secunit_capture::github;
let rt = build_runtime()?;
let _rt_guard = rt.enter();
let client = github::GhClient::from_env().map_err(map_runtime)?;
let env_and_out = match cmd {
GithubCmd::DependabotAlerts { repo, state, out } => {
let (owner, repo_name) = split_repo(&repo)?;
let env = rt
.block_on(github::dependabot_alerts::capture(
&client,
&owner,
&repo_name,
state.as_deref(),
))
.map_err(map_runtime)?;
(env, out)
}
GithubCmd::BranchProtection { repo, branch, out } => {
let (owner, repo_name) = split_repo(&repo)?;
let env = rt
.block_on(github::branch_protection::capture(
&client, &owner, &repo_name, &branch,
))
.map_err(map_runtime)?;
(env, out)
}
GithubCmd::OrgMembers { org, out } => {
let env = rt
.block_on(github::org_members::capture(&client, &org))
.map_err(map_runtime)?;
(env, out)
}
GithubCmd::AuditLog { org, since, out } => {
let env = rt
.block_on(github::audit_log::capture(&client, &org, &since))
.map_err(map_runtime)?;
(env, out)
}
GithubCmd::CodeqlAlerts { repo, out } => {
let (owner, repo_name) = split_repo(&repo)?;
let env = rt
.block_on(github::codeql_alerts::capture(&client, &owner, &repo_name))
.map_err(map_runtime)?;
(env, out)
}
};
write_envelope(&env_and_out.0, &env_and_out.1)?;
Ok(ExitCode::SUCCESS)
}
#[cfg(not(feature = "github"))]
fn github(_cmd: GithubCmd) -> Result<ExitCode> {
eprintln!("error: this binary was built without the `github` feature");
Ok(ExitCode::from(2))
}
#[cfg(any(feature = "deps", feature = "github"))]
fn write_envelope(env: &Envelope, out: &std::path::Path) -> Result<()> {
let errors = secunit_capture::schema::validate(env).context("schema lookup")?;
if !errors.is_empty() {
eprintln!("schema validation failed for `{}`:", env.capturer);
for e in &errors {
eprintln!(" {e}");
}
return Err(anyhow::anyhow!(
"capture output for `{}` failed schema validation ({} error(s))",
env.capturer,
errors.len()
));
}
env.write_to(out)
.with_context(|| format!("write envelope to {}", out.display()))?;
eprintln!("✓ wrote {}", out.display());
Ok(())
}
#[cfg(any(feature = "deps", feature = "github"))]
fn build_runtime() -> Result<tokio::runtime::Runtime> {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime")
}
#[cfg(feature = "github")]
fn split_repo(s: &str) -> Result<(String, String)> {
let (a, b) = s
.split_once('/')
.ok_or_else(|| anyhow::anyhow!("--repo must be in `owner/name` form: got `{s}`"))?;
Ok((a.to_string(), b.to_string()))
}
#[cfg(any(feature = "deps", feature = "github"))]
fn map_runtime(e: anyhow::Error) -> anyhow::Error {
e.context("capture runtime failure")
}
#[cfg(all(test, feature = "github"))]
mod tests {
use super::*;
#[test]
fn ghclient_build_does_not_panic_without_runtime_guard() {
let rt = build_runtime().expect("build runtime");
let _guard = rt.enter();
let client =
secunit_capture::github::GhClient::with_base_uri("https://example.invalid", Some("t"));
assert!(
client.is_ok(),
"client build should succeed under rt.enter(): {}",
client.err().map(|e| e.to_string()).unwrap_or_default()
);
}
}