use std::{fmt::Display, path::PathBuf};
use crate::{Config, KonarrError};
use async_trait::async_trait;
pub mod grype;
pub mod syft;
pub mod trivy;
pub use grype::Grype;
pub use syft::Syft;
pub use trivy::Trivy;
#[async_trait]
pub trait Tool {
async fn init() -> ToolConfig
where
Self: Sized;
async fn version(config: &ToolConfig) -> Result<String, KonarrError>
where
Self: Sized,
{
if let Some(path) = &config.path {
let output = tokio::process::Command::new(path)
.args(&["--version"])
.output()
.await?;
if !output.status.success() {
return Err(KonarrError::ToolError("Failed to get version".to_string()));
}
let version = String::from_utf8(output.stdout)?.replace(&config.name, "");
Ok(version.trim().to_string())
} else {
Err(KonarrError::ToolError("No tool path".to_string()))
}
}
fn find(binary: &str) -> Result<PathBuf, KonarrError>
where
Self: Sized,
{
let locations = vec![
"/usr/local/toolcache",
"/usr/local/bin/",
"/usr/bin/",
"/bin/",
"/snap/bin/",
];
for loc in locations {
let bin = PathBuf::from(loc).join(binary);
if bin.exists() {
return Ok(bin);
}
}
Err(KonarrError::ToolError(format!(
"Can not find tool {}",
binary
)))
}
async fn remote_version<'a>(config: &'a mut ToolConfig) -> Result<String, KonarrError>
where
Self: Sized;
async fn run(
config: &ToolConfig,
image: impl Into<String> + Send,
) -> Result<String, KonarrError>
where
Self: Sized;
}
pub async fn run(config: &Config, image: impl Into<String>) -> Result<String, KonarrError> {
let image = image.into();
let mut tools = ToolConfig::tools().await?;
if let Some(tool_name) = &config.agent.tool {
log::info!("Using tool: {}", tool_name);
let tool = tools
.iter_mut()
.find(|t| t.name.to_lowercase() == tool_name.to_lowercase())
.ok_or(KonarrError::ToolError(format!(
"Tool not found: {}",
tool_name
)))?;
if !tool.is_available() {
if config.agent.tool_auto_install {
log::info!("Tool is not available, trying to install: {}", tool.name);
tool.install().await?;
} else {
log::info!("Tool is not available: {}", tool.name);
return Err(KonarrError::ToolError(format!(
"Tool not available: {}",
tool_name
)));
}
}
log::info!("Tool is available: {}", tool);
log::info!("Running tool: {}", tool);
tool.run(image).await
} else {
log::info!("No tool specified, trying to find a tool");
for tool in tools.iter() {
if tool.is_available() {
log::info!("Running tool: {}", tool);
return tool.run(image).await;
}
}
Err(KonarrError::ToolError("No tools found".to_string()))
}
}
#[derive(Debug, Clone)]
pub struct ToolConfig {
pub name: String,
pub version: String,
pub remote_version: Option<String>,
pub path: Option<PathBuf>,
pub install_path: Option<PathBuf>,
pub output: PathBuf,
}
const TOOLCACHE_DIRS: &[&str] = &["/usr/local/toolcache", "/usr/local/bin/"];
impl ToolConfig {
pub fn new(name: &str, path: PathBuf) -> Self {
Self {
name: name.to_string(),
path: Some(path),
..Default::default()
}
}
pub async fn tools() -> Result<Vec<ToolConfig>, KonarrError> {
log::debug!("Getting list of available tools");
let mut tools = vec![];
tools.push(Grype::init().await);
tools.push(Syft::init().await);
tools.push(Trivy::init().await);
log::debug!("Number of tools found: {}", tools.len());
Ok(tools)
}
pub async fn find_tool(tool_name: &String) -> Result<ToolConfig, KonarrError> {
Ok(ToolConfig::tools()
.await?
.iter()
.find(|t| t.name == *tool_name)
.cloned()
.ok_or(KonarrError::ToolError(format!(
"Tool not found: {}",
tool_name
)))?)
}
pub fn is_available(&self) -> bool {
if self.path.is_some() && !self.version.is_empty() {
true
} else {
false
}
}
pub async fn run(&self, image: impl Into<String> + Send) -> Result<String, KonarrError> {
match self.name.as_str() {
"grype" => {
return Grype::run(&self, image).await;
}
"syft" => {
return Syft::run(&self, image).await;
}
"trivy" => {
return Trivy::run(&self, image).await;
}
_ => panic!("Tool not implemented"),
}
}
pub fn find(&self) -> Result<PathBuf, KonarrError> {
log::debug!("Finding tool: {}", self.name);
match self.name.as_str() {
"grype" => {
return Grype::find("grype");
}
"syft" => {
return Syft::find("syft");
}
"trivy" => {
return Trivy::find("trivy");
}
_ => panic!("Tool not implemented"),
}
}
pub async fn version(&self) -> Result<String, KonarrError> {
match self.name.as_str() {
"grype" => {
return Grype::version(&self).await;
}
"syft" => {
return Syft::version(&self).await;
}
"trivy" => {
return Trivy::version(&self).await;
}
_ => panic!("Tool not implemented"),
}
}
pub async fn remote_version(&mut self) -> Result<String, KonarrError> {
match self.name.as_str() {
"grype" => {
return Grype::remote_version(self).await;
}
"syft" => {
return Syft::remote_version(self).await;
}
"trivy" => {
return Trivy::remote_version(self).await;
}
_ => panic!("Tool not implemented"),
}
}
pub async fn install(&mut self) -> Result<(), KonarrError> {
if let Some(ipath) = &self.install_path {
log::debug!("Running install script: {}", ipath.display());
let output: PathBuf = TOOLCACHE_DIRS
.iter()
.map(|d| PathBuf::from(d))
.find(|d| {
d.is_dir() && d.exists() && !d.metadata().unwrap().permissions().readonly()
})
.unwrap_or_else(|| std::env::temp_dir());
log::debug!("Toolcache directory: {}", output.display());
tokio::process::Command::new("sh")
.arg(ipath)
.args(&["-b", output.display().to_string().as_str()])
.output()
.await
.map_err(|e| {
KonarrError::ToolError(format!("Failed to run install script: {}", e))
})?;
log::info!("Successfully installed {}", self.name);
self.path = self.find().ok();
if let Ok(version) = self.version().await {
self.version = version;
}
Ok(())
} else {
Err(KonarrError::ToolError(
"No install script found".to_string(),
))
}
}
pub async fn read_output(&self) -> Result<String, KonarrError> {
tokio::fs::read_to_string(&self.output)
.await
.map_err(|e| KonarrError::ToolError(format!("Failed to read output: {}", e)))
}
pub(crate) async fn github_release(
&mut self,
repository: impl Into<String>,
) -> Result<String, KonarrError> {
let repository = repository.into();
let url = format!(
"https://api.github.com/repos/{}/releases/latest",
repository
);
log::debug!("Getting release from GitHub: {}", url);
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(
reqwest::header::ACCEPT,
reqwest::header::HeaderValue::from_static("application/vnd.github.v3+json"),
);
let client = reqwest::Client::builder()
.user_agent(format!("Konarr/{}", crate::KONARR_VERSION))
.default_headers(headers)
.build()?;
let response = client.get(&url).send().await?;
if !response.status().is_success() {
log::error!("Failed to get release from GitHub: {}", repository);
return Err(KonarrError::ToolError(format!(
"Failed to get release: {}",
response.status()
)));
}
let json: serde_json::Value = response.json().await?;
let version = json["tag_name"]
.as_str()
.ok_or(KonarrError::ToolError("No tag_name".to_string()))?;
self.remote_version = Some(version.to_string());
Ok(version.to_string())
}
}
impl Default for ToolConfig {
fn default() -> Self {
let temp_path = std::env::temp_dir().join("konarr");
let output = temp_path.join(format!("unknown-{}.json", chrono::Utc::now().timestamp()));
if let Some(parent) = output.parent() {
let _ = std::fs::create_dir_all(parent);
}
Self {
name: "".to_string(),
version: "".to_string(),
path: None,
install_path: None,
remote_version: None,
output,
}
}
}
impl Display for ToolConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_available() {
write!(f, "{} ({})", self.name, self.version)
} else {
write!(f, "{} (Not installed)", self.name)
}
}
}