Skip to content

Commit

Permalink
chore: refactor code to support both query and collect commands from CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
dejanb committed Jun 19, 2023
1 parent dd1607f commit a8541be
Show file tree
Hide file tree
Showing 10 changed files with 236 additions and 112 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The following are examples of commands that CLI (and thus the library as well) a
Returns purls of all known dependencies of the provided purl.

```
$ guac dependencies pkg:maven/io.vertx/vertx-web@4.3.7
$ guac query dependencies pkg:maven/io.vertx/vertx-web@4.3.7
[
"pkg:maven/io.vertx/vertx-web-common@4.3.7",
"pkg:maven/io.vertx/vertx-auth-common@4.3.7",
Expand All @@ -42,7 +42,7 @@ $ guac dependencies pkg:maven/io.vertx/vertx-web@4.3.7
Returns purls of all known dependents for the provided purl.

```
$ guac dependents pkg:maven/io.vertx/vertx-web@4.3.7
$ guac query dependents pkg:maven/io.vertx/vertx-web@4.3.7
[
"pkg:maven/io.seedwing/seedwing-java-example@1.0.0-SNAPSHOT?type=jar",
"pkg:maven/io.quarkus.resteasy.reactive/resteasy-reactive-vertx@2.16.2.Final?type=jar",
Expand All @@ -57,7 +57,7 @@ $ guac dependents pkg:maven/io.vertx/vertx-web@4.3.7
Returns list of all known vulnerabilities for the provided purl

```
$ guac vulnerabilities pkg:rpm/redhat/openssl@1.1.1k-7.el8_6
$ guac query vulnerabilities pkg:rpm/redhat/openssl@1.1.1k-7.el8_6
[
{
"cve": "cve-2023-0286",
Expand All @@ -76,7 +76,7 @@ $ guac vulnerabilities pkg:rpm/redhat/openssl@1.1.1k-7.el8_6
Returns list of all versions for the given package purl

```
$ guac packages pkg:maven/io.vertx/vertx-web
$ guac query packages pkg:maven/io.vertx/vertx-web
[
"pkg:maven/io.vertx/vertx-web@4.3.7?type=jar",
"pkg:maven/io.vertx/vertx-web@4.3.4.redhat-00007?type=jar"
Expand Down
1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ serde_json = "1.0.56"
colored_json = "3.0.1"
clap = { version = "4.0.29", features = ["derive"] }
nats = "0.24.0"
exporter = { git = "https://github.com/trustification/trustification.git" }

[[bin]]
name = "guac"
Expand Down
29 changes: 29 additions & 0 deletions cli/src/collect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::process::ExitCode;

use anyhow::*;
use guac::collector::{
collector::{Collector, FileCollector},
emitter::NatsEmitter,
};

#[derive(clap::Subcommand, Debug)]
pub enum CollectCommand {
File { path: String },
S3 {},
}

impl CollectCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
println!("Collecting ...");

let emitter = NatsEmitter::new("127.0.0.1:4222").await?;

let collector = FileCollector {
path: "example/seedwing-java-example.bom".to_string(),
};

collector.run(emitter).await?;

Ok(ExitCode::SUCCESS)
}
}
17 changes: 0 additions & 17 deletions cli/src/collector.rs

This file was deleted.

123 changes: 40 additions & 83 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
use anyhow::*;
use clap::{ColorChoice, Parser, Subcommand};
use guac::graphql::{client::GuacClient, vulns2vex};
use std::process::{ExitCode, Termination};

use colored_json::{prelude::*, Output};
use clap::Parser;

pub mod collect;
pub mod query;

#[derive(clap::Subcommand, Debug)]
pub enum Command {
#[command(subcommand)]
Query(query::QueryCommand),
#[command(subcommand)]
Collect(collect::CollectCommand),
}

#[derive(Parser, Debug)]
#[command(
Expand All @@ -12,94 +21,42 @@ use colored_json::{prelude::*, Output};
long_about = None
)]
pub struct Cli {
#[arg(
short = 'g',
long = "guac",
default_value = "http://localhost:8080/query"
)]
pub(crate) guac_url: String,

#[arg(short = 'c', long = "color", default_value = "auto")]
color: ColorChoice,

#[command(subcommand)]
pub(crate) command: Commands,
pub(crate) command: Command,
}

// TODO: group arguments for guac related commands (url, purl, color, ...)
#[derive(Subcommand, Debug)]
enum Commands {
/// get all dependencies for the provided purl.
Dependencies {
/// Artifact purl
purl: String,
},
/// get all dependents on the provided purl.
Dependents {
/// Artifact purl
purl: String,
},
/// get all versions for the provided package purl.
Packages {
/// Artifact purl
purl: String,
},
/// get all known vulnerabilities for the provided purl.
Vulnerabilities {
/// Artifact purl
purl: String,
/// Return VEX document
#[arg(short = 'v', long = "vex", default_value = "false")]
vex: bool,
},
impl Cli {
async fn run(self) -> ExitCode {
match self.run_command().await {
Ok(code) => code,
Err(err) => {
eprintln!("Error: {err}");
for (n, err) in err.chain().skip(1).enumerate() {
if n == 0 {
eprintln!("Caused by:");
}
eprintln!("\t{err}");
}

ExitCode::FAILURE
}
}
}

async fn run_command(self) -> anyhow::Result<ExitCode> {
match self.command {
Command::Query(run) => run.run().await,
Command::Collect(run) => run.run().await,
}
}
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
async fn main() -> impl Termination {
//let purl = "pkg:maven/io.vertx/vertx-web@4.3.7";
//let purl = "pkg:deb/debian";
//let purl = "pkg:pypi/django";
//let purl = "pkg:rpm/redhat/openssl@1.1.1k-7.el8_6";

let cli = Cli::parse();
let guac = GuacClient::new(cli.guac_url);

match cli.command {
Commands::Dependencies { purl } => {
let deps = guac.get_dependencies(&purl).await?;
let out = serde_json::to_string(&deps)?.to_colored_json(color_mode(cli.color))?;
println!("{}", out);
}
Commands::Dependents { purl } => {
let deps = guac.is_dependent(&purl).await?;
let out = serde_json::to_string(&deps)?.to_colored_json(color_mode(cli.color))?;
println!("{}", out);
}
Commands::Packages { purl } => {
// e.g. "pkg:maven/io.vertx/vertx-web"
let pkgs = guac.get_packages(&purl).await?;
let out = serde_json::to_string(&pkgs)?.to_colored_json(color_mode(cli.color))?;
println!("{}", out);
}
Commands::Vulnerabilities { purl, vex } => {
let vulns = guac.certify_vuln(&purl).await?;
let out = if vex {
let vex = vulns2vex(vulns);
serde_json::to_string(&vex)?.to_colored_json(color_mode(cli.color))?
} else {
serde_json::to_string(&vulns)?.to_colored_json(color_mode(cli.color))?
};
println!("{}", out);
}
}

Ok(())
}

fn color_mode(choice: ColorChoice) -> ColorMode {
match choice {
ColorChoice::Auto => ColorMode::Auto(Output::StdOut),
ColorChoice::Always => ColorMode::On,
ColorChoice::Never => ColorMode::Off,
}
Cli::parse().run().await
}
153 changes: 153 additions & 0 deletions cli/src/query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use std::process::ExitCode;

use clap::{ColorChoice, Subcommand};
use guac::graphql::{client::GuacClient, vulns2vex};

use colored_json::{prelude::*, Output};

#[derive(Subcommand, Debug)]
pub enum QueryCommand {
Dependencies(DependenciesCommand),
Dependents(DependentsCommand),
Packages(PackagesCommand),
Vulnerabilities(VulnerabilitiesCommand),
}

#[derive(Clone, Debug, clap::Parser)]
#[command(rename_all_env = "SCREAMING_SNAKE_CASE")]
pub struct QueryConfig {
#[arg(
short = 'g',
long = "guac",
default_value = "http://localhost:8080/query"
)]
pub(crate) guac_url: String,

#[arg(short = 'c', long = "color", default_value = "auto")]
color: ColorChoice,

purl: String,
}

impl QueryCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
match self {
Self::Dependencies(command) => command.run().await,
Self::Dependents(command) => command.run().await,
Self::Packages(command) => command.run().await,
Self::Vulnerabilities(command) => command.run().await,
}
}
}

#[derive(clap::Args, Debug)]
#[command(
about = "Run the query to find all dependencies of the package (purl)",
args_conflicts_with_subcommands = true
)]
pub struct DependenciesCommand {
#[command(flatten)]
pub(crate) config: QueryConfig,
}

impl DependenciesCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
let guac = GuacClient::new(self.config.guac_url);
let deps = guac.get_dependencies(&self.config.purl).await?;
let out = serde_json::to_string(&deps)?.to_colored_json(color_mode(self.config.color))?;
println!("{}", out);
Ok(ExitCode::SUCCESS)
}
}

#[derive(clap::Args, Debug)]
#[command(
about = "Run the query to find all dependents of the package (purl)",
args_conflicts_with_subcommands = true
)]
pub struct DependentsCommand {
#[command(flatten)]
pub(crate) config: QueryConfig,
}

impl DependentsCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
let guac = GuacClient::new(self.config.guac_url);
let deps = guac.is_dependent(&self.config.purl).await?;
let out = serde_json::to_string(&deps)?.to_colored_json(color_mode(self.config.color))?;
println!("{}", out);
Ok(ExitCode::SUCCESS)
}
}

#[derive(clap::Args, Debug)]
#[command(
about = "Run the query to find all related packages of the package (purl)",
args_conflicts_with_subcommands = true
)]
pub struct PackagesCommand {
#[command(flatten)]
pub(crate) config: QueryConfig,
}

impl PackagesCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
let guac = GuacClient::new(self.config.guac_url);
let pkgs = guac.get_packages(&self.config.purl).await?;
let out = serde_json::to_string(&pkgs)?.to_colored_json(color_mode(self.config.color))?;
println!("{}", out);
Ok(ExitCode::SUCCESS)
}
}

#[derive(Clone, Debug, clap::Parser)]
#[command(rename_all_env = "SCREAMING_SNAKE_CASE")]
pub struct VulnerabilitiesConfig {
#[arg(
short = 'g',
long = "guac",
default_value = "http://localhost:8080/query"
)]
pub(crate) guac_url: String,

#[arg(short = 'c', long = "color", default_value = "auto")]
color: ColorChoice,

purl: String,

#[arg(short = 'v', long = "vex", default_value = "false")]
vex: bool,
}

#[derive(clap::Args, Debug)]
#[command(
about = "Run the query to find all related packages of the package (purl)",
args_conflicts_with_subcommands = true
)]
pub struct VulnerabilitiesCommand {
#[command(flatten)]
pub(crate) config: VulnerabilitiesConfig,
}

impl VulnerabilitiesCommand {
pub async fn run(self) -> anyhow::Result<ExitCode> {
let guac = GuacClient::new(self.config.guac_url);
let vulns = guac.certify_vuln(&self.config.purl).await?;
let out = if self.config.vex {
let vex = vulns2vex(vulns);
serde_json::to_string(&vex)?.to_colored_json(color_mode(self.config.color))?
} else {
serde_json::to_string(&vulns)?.to_colored_json(color_mode(self.config.color))?
};
println!("{}", out);
Ok(ExitCode::SUCCESS)
}
}

fn color_mode(choice: ColorChoice) -> ColorMode {
match choice {
ColorChoice::Auto => ColorMode::Auto(Output::StdOut),
ColorChoice::Always => ColorMode::On,
ColorChoice::Never => ColorMode::Off,
}
}
Loading

0 comments on commit a8541be

Please sign in to comment.