pub mod cart;
pub mod report;
pub mod retrieval;
pub mod search;
pub mod similar;
pub mod submit;
use crate::MdbClient;
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::Result;
use clap::{Command, CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};
use dialoguer::Password;
pub const VERSION: &str = concat!(env!("MDB_VERSION"), " ", env!("MDB_BUILD_DATE"));
#[derive(Debug, Parser)]
#[command(author, about, version = VERSION)]
pub struct Options {
#[clap(subcommand)]
cmd: Subcommands,
}
impl Options {
#[allow(clippy::too_many_lines)]
pub async fn execute(&self) -> Result<ExitCode> {
if let Subcommands::Login(cmd) = &self.cmd {
return cmd.execute().await;
}
if let Subcommands::Cart(cmd) = &self.cmd {
return cmd.execute();
}
if let Subcommands::Generate(cmd) = &self.cmd {
return Ok(cmd.execute());
}
if self.cmd == Subcommands::Discover {
let servers = malwaredb_client::discover_servers()?;
if servers.is_empty() {
println!("No MalwareDB servers found.");
} else {
for server in servers {
let extra = if let Ok(info) = server.server_info().await {
format!(
" — Version {} with {} samples, {}",
info.mdb_version, info.num_samples, info.db_size
)
} else {
String::new()
};
println!("{}\t{server}{extra}", server.name);
}
}
return Ok(ExitCode::SUCCESS);
}
let mdb_client = MdbClient::load()?;
match &self.cmd {
Subcommands::Whoami => {
let resp = mdb_client.whoami().await?;
println!("UserID: {}", resp.id);
if resp.groups.is_empty() {
println!("You aren't part of any groups.");
} else {
println!("You're part of {} groups:", resp.groups.len());
for group in resp.groups {
println!("\t{group}");
}
}
if !resp.sources.is_empty() {
println!("You have access to {} sources:", resp.sources.len());
for source in resp.sources {
println!("\t{source}");
}
}
println!("Account creation: {}", resp.created);
if resp.is_readonly {
println!("You are a read-only user.");
}
if resp.is_admin {
println!("You are an administrator of this MalwareDB instance.");
}
Ok(ExitCode::SUCCESS)
}
Subcommands::Login(_)
| Subcommands::Cart(_)
| Subcommands::Generate(_)
| Subcommands::Discover => {
unreachable!()
}
Subcommands::Logout => {
mdb_client.delete()?;
Ok(ExitCode::SUCCESS)
}
Subcommands::ResetKey => {
mdb_client.reset_key().await?;
mdb_client.delete()?;
Ok(ExitCode::SUCCESS)
}
Subcommands::ServerInfo => {
let resp = mdb_client.server_info().await?;
println!("-- {} --", resp.instance_name);
println!("MalwareDB version {} on {}", resp.mdb_version, resp.os_name);
println!("Memory Used: {}", resp.memory_used);
println!("Users: {}", resp.num_users);
println!("Samples: {}", resp.num_samples);
println!("Database size: {}", resp.db_size);
println!("Uptime: {}", resp.uptime);
if resp.mdb_version > *malwaredb_client::MDB_VERSION_SEMVER {
eprintln!(
"Server version {:?} is newer than client {:?}, consider updating.",
resp.mdb_version,
malwaredb_client::MDB_VERSION_SEMVER
);
}
Ok(ExitCode::SUCCESS)
}
Subcommands::ServerTypes => {
let resp = mdb_client.supported_types().await?;
for data_type in resp.types {
print!("{}", data_type.name);
if let Some(desc) = data_type.description {
print!(" {desc}");
}
if data_type.is_executable {
print!(" -- is executable");
}
println!();
for magic in data_type.magic {
println!("\t{magic}");
}
}
Ok(ExitCode::SUCCESS)
}
Subcommands::ListLabels => {
let labels = mdb_client.labels().await?;
println!("{labels}");
Ok(ExitCode::SUCCESS)
}
Subcommands::ListSources => {
let sources = mdb_client.sources().await?;
if sources.sources.is_empty() {
println!("No sources available, are you part of any groups?");
}
for source in sources.sources {
print!("{}: {}", source.id, source.name);
if let Some(url) = source.url {
print!(" {url}");
}
if let Some(malicious) = source.malicious {
if malicious {
print!(" - malicious!");
}
}
println!();
}
Ok(ExitCode::SUCCESS)
}
Subcommands::SubmitSamples(cmd) => cmd.exec(&mdb_client).await,
Subcommands::SampleReport(cmd) => cmd.exec(&mdb_client).await,
Subcommands::RetrieveSample(cmd) => cmd.exec(&mdb_client).await,
Subcommands::FindSimilar(cmd) => cmd.exec(&mdb_client).await,
Subcommands::Search(cmd) => cmd.exec(&mdb_client).await,
Subcommands::YaraSearch(cmd) => cmd.exec(&mdb_client).await,
Subcommands::YaraResult(cmd) => cmd.exec(&mdb_client).await,
}
}
}
#[derive(Subcommand, Clone, Debug, PartialEq)]
pub enum Subcommands {
Discover,
Whoami,
Login(Login),
Logout,
ResetKey,
ServerInfo,
ServerTypes,
ListLabels,
ListSources,
SubmitSamples(submit::SubmitSamples),
RetrieveSample(retrieval::RetrieveSample),
SampleReport(report::SampleReport),
FindSimilar(similar::Similar),
Cart(cart::CartIO),
Generate(Generator),
Search(search::SearchRequest),
YaraSearch(search::YaraSearch),
YaraResult(search::YaraResult),
}
#[derive(Clone, Debug, Parser, PartialEq)]
pub struct Login {
pub url: String,
pub uname: String,
pub cert_path: Option<PathBuf>,
}
impl Login {
async fn execute(&self) -> Result<ExitCode> {
let password = Password::new()
.with_prompt(format!("Password for {}", self.uname))
.interact()?;
if let Err(e) = MdbClient::login(
self.url.clone(),
self.uname.clone(),
password,
true,
self.cert_path.clone(),
)
.await
{
eprintln!("{e}");
Ok(ExitCode::FAILURE)
} else {
Ok(ExitCode::SUCCESS)
}
}
}
#[derive(Parser, Debug, Clone, PartialEq)]
pub struct Generator {
#[arg(value_enum)]
pub(crate) generator: Shell,
}
impl Generator {
pub fn execute(&self) -> ExitCode {
let mut cmd = Options::command();
eprintln!("Generating completion file for {:?}...", self.generator);
print_completions(self.generator, &mut cmd);
ExitCode::SUCCESS
}
}
fn print_completions<G: clap_complete::Generator>(gen: G, cmd: &mut Command) {
generate(gen, cmd, cmd.get_name().to_string(), &mut std::io::stdout());
}
#[test]
fn verify_cli() {
Options::command().debug_assert();
Login::command().debug_assert();
}