From a2cdbd7a91cc0e74bcfa3b33d81745cb93354f88 Mon Sep 17 00:00:00 2001 From: Wesley Shields Date: Mon, 22 Jul 2024 18:35:55 -0400 Subject: [PATCH] feat: Add json output to scan command. When using scan command you can now output in json or json-pretty format. The former is just a single JSON string per line while the latter is a pretty-printed JSON object. Due to the output subsystem you can do things like: ./yr scan --output-format json rules/test.yara /bin | jq .path and it will work, but you won't see the information about currently scanning files or the "final" message which tells you the number of files scanned. I'm guessing this is because the superscan is aware that stdout is being redirected somehow? Either way, this is a nice feature because it means we get the ability to pipe the JSON output to things like jq easily. --- cli/src/commands/scan.rs | 185 +++++++++++++++++++++++++++++---------- cli/src/help.rs | 5 ++ 2 files changed, 144 insertions(+), 46 deletions(-) diff --git a/cli/src/commands/scan.rs b/cli/src/commands/scan.rs index 7ee963a6f..84fa65d0f 100644 --- a/cli/src/commands/scan.rs +++ b/cli/src/commands/scan.rs @@ -6,7 +6,7 @@ use std::sync::Mutex; use std::time::{Duration, Instant}; use anyhow::{bail, Context, Error}; -use clap::{arg, value_parser, ArgAction, ArgMatches, Command}; +use clap::{arg, value_parser, ArgAction, ArgMatches, Command, ValueEnum}; use crossbeam::channel::Sender; use superconsole::style::Stylize; use superconsole::{Component, Line, Lines, Span}; @@ -20,6 +20,15 @@ use crate::commands::{ use crate::walk::Message; use crate::{help, walk}; +#[derive(Clone, ValueEnum)] +enum OutputFormats { + Text, + Json, + JsonPretty, +} + +const STRINGS_LIMIT: usize = 120; + #[rustfmt::skip] pub fn scan() -> Command { super::command("scan") @@ -112,6 +121,13 @@ pub fn scan() -> Command { .value_parser(external_var_parser) .action(ArgAction::Append) ) + .arg( + arg!(-o --"output-format" ) + .help("Output format for results") + .long_help(help::OUTPUT_FORMAT_LONG_HELP) + .required(false) + .value_parser(value_parser!(OutputFormats)) + ) } pub fn exec_scan(args: &ArgMatches) -> anyhow::Result<()> { @@ -334,63 +350,140 @@ fn print_matching_rules( let print_namespace = args.get_flag("print-namespace"); let print_strings = args.get_flag("print-strings"); let print_strings_limit = args.get_one::("print-strings-limit"); + let output_format = args.get_one::("output-format"); // Clippy insists on replacing the `while let` statement with // `for matching_rule in rules.by_ref()`, but that fails with // `the `by_ref` method cannot be invoked on a trait object` #[allow(clippy::while_let_on_iterator)] while let Some(matching_rule) = rules.next() { - let line = if print_namespace { - format!( - "{}:{} {}", - matching_rule.namespace().paint(Cyan).bold(), - matching_rule.identifier().paint(Cyan).bold(), - file_path.display(), - ) - } else { - format!( - "{} {}", - matching_rule.identifier().paint(Cyan).bold(), - file_path.display() - ) - }; - - output.send(Message::Info(line)).unwrap(); - - if print_strings || print_strings_limit.is_some() { - let limit = print_strings_limit.unwrap_or(&120); - for p in matching_rule.patterns() { - for m in p.matches() { - let match_range = m.range(); - let match_data = m.data(); - - let mut msg = format!( - "{:#x}:{}:{}: ", - match_range.start, - match_range.len(), - p.identifier(), - ); - - for b in &match_data[..min(match_data.len(), *limit)] { - for c in b.escape_ascii() { - msg.push_str(format!("{}", c as char).as_str()); + match output_format { + Some(OutputFormats::Json) | Some(OutputFormats::JsonPretty) => { + let mut json = if print_namespace { + serde_json::json!({ + "path": file_path.to_str().unwrap(), + "namespace": matching_rule.namespace(), + "identifier": matching_rule.identifier() + }) + } else { + serde_json::json!({ + "path": file_path.to_str().unwrap(), + "identifier": matching_rule.identifier() + }) + }; + + if print_strings || print_strings_limit.is_some() { + let limit = print_strings_limit.unwrap_or(&STRINGS_LIMIT); + for p in matching_rule.patterns() { + let mut match_vec: Vec = Vec::new(); + for m in p.matches() { + let match_range = m.range(); + let match_data = m.data(); + + let mut s = String::new(); + + for b in + &match_data[..min(match_data.len(), *limit)] + { + for c in b.escape_ascii() { + s.push_str( + format!("{}", c as char).as_str(), + ); + } + } + + if match_data.len() > *limit { + s.push_str( + format!( + " ... {} more bytes", + match_data + .len() + .saturating_sub(*limit) + ) + .as_str(), + ); + } + let match_json = serde_json::json!({ + "identifier": p.identifier(), + "start": match_range.start, + "length": match_range.len(), + "data": s.as_str() + }); + match_vec.push(match_json); } + json["strings"] = serde_json::json!(match_vec); } + } - if match_data.len() > *limit { - msg.push_str( - format!( - " ... {} more bytes", - match_data.len().saturating_sub(*limit) - ) - .as_str(), - ); + match output_format { + Some(OutputFormats::Json) => output + .send(Message::Info(format!("{}", json))) + .unwrap(), + Some(OutputFormats::JsonPretty) => output + .send(Message::Info(format!("{:#}", json))) + .unwrap(), + _ => unreachable!(), + }; + } + Some(OutputFormats::Text) | None => { + let line = if print_namespace { + format!( + "{}:{} {}", + matching_rule.namespace().paint(Cyan).bold(), + matching_rule.identifier().paint(Cyan).bold(), + file_path.display(), + ) + } else { + format!( + "{} {}", + matching_rule.identifier().paint(Cyan).bold(), + file_path.display() + ) + }; + output.send(Message::Info(line)).unwrap(); + + if print_strings || print_strings_limit.is_some() { + let limit = print_strings_limit.unwrap_or(&STRINGS_LIMIT); + for p in matching_rule.patterns() { + for m in p.matches() { + let match_range = m.range(); + let match_data = m.data(); + + let mut msg = format!( + "{:#x}:{}:{}: ", + match_range.start, + match_range.len(), + p.identifier(), + ); + + for b in + &match_data[..min(match_data.len(), *limit)] + { + for c in b.escape_ascii() { + msg.push_str( + format!("{}", c as char).as_str(), + ); + } + } + + if match_data.len() > *limit { + msg.push_str( + format!( + " ... {} more bytes", + match_data + .len() + .saturating_sub(*limit) + ) + .as_str(), + ); + } + + output.send(Message::Info(msg)).unwrap(); + } } - - output.send(Message::Info(msg)).unwrap(); } } - } + }; } } diff --git a/cli/src/help.rs b/cli/src/help.rs index b65a915aa..188916aaa 100644 --- a/cli/src/help.rs +++ b/cli/src/help.rs @@ -108,3 +108,8 @@ Examples: yr scan rules_file.yar scanned_file yr scan rules_dir scanned_file"#; + +pub const OUTPUT_FORMAT_LONG_HELP: &str = r#"Output format + +The format in which results will be displayed. Any errors or warnings will not be +in this format, only results."#;