Skip to content

Commit

Permalink
feat: Add json output to scan command.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wxsBSD committed Jul 22, 2024
1 parent 7df210b commit a2cdbd7
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 46 deletions.
185 changes: 139 additions & 46 deletions cli/src/commands/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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")
Expand Down Expand Up @@ -112,6 +121,13 @@ pub fn scan() -> Command {
.value_parser(external_var_parser)
.action(ArgAction::Append)
)
.arg(
arg!(-o --"output-format" <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<()> {
Expand Down Expand Up @@ -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::<usize>("print-strings-limit");
let output_format = args.get_one::<OutputFormats>("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<serde_json::Value> = 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();
}
}
}
};
}
}

Expand Down
5 changes: 5 additions & 0 deletions cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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."#;

0 comments on commit a2cdbd7

Please sign in to comment.