Skip to content

Commit

Permalink
Add a new report --min-score=SCORE command-line option (#184)
Browse files Browse the repository at this point in the history
* Add a new `report --min-score=SCORE` command-line option

* Update CHANGELOG with link
  • Loading branch information
bradlarsen committed May 13, 2024
1 parent 8a90eac commit 23b9e7a
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 23 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- The README now includes several animated GIFs that demonstrate simple example use cases ([#154](https://github.com/praetorian-inc/noseyparker/pull/154)).

- The `report` command now offers a new `--finding-status=STATUS` filtering option, which causes only the findings with the requested status to be reported ([#162](https://github.com/praetorian-inc/noseyparker/pull/162)).
- The `report` command now offers a new `--finding-status=STATUS` filtering option ([#162](https://github.com/praetorian-inc/noseyparker/pull/162)).
This option causes findings with an assigned status that does not match `STATUS` to be suppressed from the report.

- The `report` command now offers a new `--min-score=SCORE` filtering option ([#184](https://github.com/praetorian-inc/noseyparker/pull/184)).
This option causes findings that have a mean score less than `SCORE` to be suppressed from the report.
This option is set by default with a value of 0.05.

- A new `datastore export` command has been added ([#166](https://github.com/praetorian-inc/noseyparker/pull/166)).
This command exports the essential content from a Nosey Parker datastore as a .tgz file that can be extracted wherever it is needed.
Expand Down
11 changes: 10 additions & 1 deletion crates/noseyparker-cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,8 +921,17 @@ pub struct ReportFilterArgs {
)]
pub max_matches: i64,

/// Only report findings that have a mean score of at least N.
///
/// Scores are floating point numbers in the range [0, 1].
/// Use the value `0` to disable this filtering.
///
/// Findings that do not have a score computed will be included regardless of this setting.
#[arg(long, default_value_t = 0.05, value_name = "SCORE")]
pub min_score: f64,

/// Include only findings with the assigned status
#[arg(long)]
#[arg(long, value_name = "STATUS")]
pub finding_status: Option<FindingStatus>,
}

Expand Down
100 changes: 85 additions & 15 deletions crates/noseyparker-cli/src/cmd_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use indenter::indented;
use schemars::JsonSchema;
use serde::Serialize;
use std::fmt::{Display, Formatter, Write};
use tracing::info;

use noseyparker::blob_metadata::BlobMetadata;
use noseyparker::bstring_escape::Escaped;
Expand Down Expand Up @@ -36,6 +37,12 @@ pub fn run(global_args: &GlobalArgs, args: &ReportArgs) -> Result<()> {
Some(args.filter_args.max_matches.try_into().unwrap())
};

let min_score = if args.filter_args.min_score <= 0.0 {
None
} else {
Some(args.filter_args.min_score)
};

// enable output styling:
// - if the output destination is not explicitly specified and colors are not disabled
// - if the output destination *is* explicitly specified and colors are forced on
Expand All @@ -50,6 +57,7 @@ pub fn run(global_args: &GlobalArgs, args: &ReportArgs) -> Result<()> {
let reporter = DetailsReporter {
datastore,
max_matches,
min_score,
finding_status: args.filter_args.finding_status,
styles,
};
Expand All @@ -59,36 +67,98 @@ pub fn run(global_args: &GlobalArgs, args: &ReportArgs) -> Result<()> {
struct DetailsReporter {
datastore: Datastore,
max_matches: Option<usize>,
min_score: Option<f64>,
finding_status: Option<FindingStatus>,
styles: Styles,
}

impl DetailsReporter {
fn include_finding(&self, metadata: &FindingMetadata) -> bool {
match self.finding_status {
None => true,
Some(status) => matches!(
(status, metadata.statuses.0.as_slice()),
(FindingStatus::Accept, &[Status::Accept])
| (FindingStatus::Reject, &[Status::Reject])
| (FindingStatus::Null, &[])
| (FindingStatus::Mixed, &[Status::Accept, Status::Reject])
| (FindingStatus::Mixed, &[Status::Reject, Status::Accept])
),
}
}
/// Does `requested_status` match the given set of statuses?
fn statuses_match(requested_status: FindingStatus, statuses: &[Status]) -> bool {
matches!(
(requested_status, statuses),
(FindingStatus::Accept, &[Status::Accept])
| (FindingStatus::Reject, &[Status::Reject])
| (FindingStatus::Null, &[])
| (FindingStatus::Mixed, &[Status::Accept, Status::Reject])
| (FindingStatus::Mixed, &[Status::Reject, Status::Accept])
)
}

impl DetailsReporter {
/// Get the metadata for all the findings that remain after filtering.
fn get_finding_metadata(&self) -> Result<Vec<FindingMetadata>> {
let datastore = &self.datastore;
let mut group_metadata = datastore
.get_finding_metadata()
.context("Failed to get match group metadata from datastore")?;

group_metadata.retain(|md| self.include_finding(md));
// How many findings were suppressed due to their status not matching?
let mut num_suppressed_for_status: usize = 0;

// How many findings were suppressed due to their status not matching?
let mut num_suppressed_for_score: usize = 0;

// Suppress findings with non-matching status
if let Some(status) = self.finding_status {
group_metadata.retain(|md| {
if statuses_match(status, md.statuses.0.as_slice()) {
true
} else {
num_suppressed_for_status += 1;
false
}
})
}

// Suppress findings with non-matching score
if let Some(min_score) = self.min_score {
group_metadata.retain(|md| match md.mean_score {
Some(mean_score) if mean_score < min_score => {
num_suppressed_for_score += 1;
false
}
_ => true,
})
}

if num_suppressed_for_status > 0 {
let finding_status = self.finding_status.unwrap();

if num_suppressed_for_status == 1 {
info!(
"Note: 1 finding with status not matching {finding_status} was suppressed; \
rerun without `--finding-status={finding_status}` to show it"
);
} else {
info!(
"Note: {num_suppressed_for_status} findings with status not matching \
`{finding_status}` were suppressed; \
rerun without `--finding-status={finding_status}` to show them"
);
}
}

if num_suppressed_for_score > 0 {
let min_score = self.min_score.unwrap();

if num_suppressed_for_status == 1 {
info!(
"Note: 1 finding with meanscore less than {min_score} was suppressed; \
rerun with `--min-score=0` to show it"
);
} else {
info!(
"Note: {num_suppressed_for_score} findings with mean score less than \
{min_score} were suppressed; \
rerun with `--min-score=0` to show them"
);
}
}

Ok(group_metadata)
}

/// Get the matches associated with the given finding.
fn get_matches(&self, metadata: &FindingMetadata) -> Result<Vec<ReportMatch>> {
Ok(self
.datastore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,17 @@ Filtering Options:

[default: 3]

--finding-status <FINDING_STATUS>
--min-score <SCORE>
Only report findings that have a mean score of at least N.

Scores are floating point numbers in the range [0, 1]. Use the value `0` to disable this
filtering.

Findings that do not have a score computed will be included regardless of this setting.

[default: 0.05]

--finding-status <STATUS>
Include only findings with the assigned status

Possible values:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ Options:
-h, --help Print help (see more with '--help')

Filtering Options:
--max-matches <N>
Limit the number of matches per finding to at most N [default: 3]
--finding-status <FINDING_STATUS>
Include only findings with the assigned status [possible values: accept, reject, mixed,
null]
--max-matches <N> Limit the number of matches per finding to at most N [default: 3]
--min-score <SCORE> Only report findings that have a mean score of at least N [default:
0.05]
--finding-status <STATUS> Include only findings with the assigned status [possible values:
accept, reject, mixed, null]

Output Options:
-o, --output <PATH> Write output to the specified path
Expand Down

0 comments on commit 23b9e7a

Please sign in to comment.