forked from mirrors/kingfisher
- Added kingfisher:ignore (or kingfisher:allow) to silence a finding inline within a file
- Added: to reuse existing inline directives from other scanners, pass --compat-ignore-comments to also accept NOSONAR, kics-scan ignore, gitleaks:allow and trufflehog:ignore
This commit is contained in:
parent
dbb97bdcf3
commit
a003b732fa
21 changed files with 507 additions and 6 deletions
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.57.0]
|
||||
- Added inline ignore directive detection to treat suppression tokens anywhere on surrounding lines, including multi-line handling
|
||||
- Added a `--no-ignore` CLI flag to disable inline directives when you need every potential secret reported
|
||||
- Added: `--compat-ignore-comments` to reuse existing inline directives from other scanners: NOSONAR, kics-scan ignore, gitleaks:allow and trufflehog:ignore
|
||||
|
||||
## [v1.56.0]
|
||||
- Fixed tree-sitter scanning bug where passing --no-base64 caused errors to be printed when the file type couldn’t be determined
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ publish = false
|
|||
|
||||
[package]
|
||||
name = "kingfisher"
|
||||
version = "1.56.0"
|
||||
version = "1.57.0"
|
||||
description = "MongoDB's blazingly fast and accurate secret scanning and validation tool"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -117,6 +117,7 @@ See ([docs/COMPARISON.md](docs/COMPARISON.md))
|
|||
- [Notable Scan Options](#notable-scan-options)
|
||||
- [Understanding `--confidence`](#understanding---confidence)
|
||||
- [Ignore known false positives](#ignore-known-false-positives)
|
||||
- [Inline ignore directives](#inline-ignore-directives)
|
||||
- [Finding Fingerprint](#finding-fingerprint)
|
||||
- [Rule Performance Profiling](#rule-performance-profiling)
|
||||
- [CLI Options](#cli-options)
|
||||
|
|
@ -962,6 +963,8 @@ leaves the default unchanged.
|
|||
- `--manage-baseline`: Create or update the baseline file with current findings
|
||||
- `--skip-regex <PATTERN>`: Ignore findings whose text matches this regex (repeatable)
|
||||
- `--skip-word <WORD>`: Ignore findings containing this case-insensitive word (repeatable)
|
||||
- `--compat-ignore-comments`: Honor inline directives from other scanners (treat `gitleaks:allow` and `trufflehog:ignore` like native suppressions)
|
||||
- `--no-ignore`: Disable inline directives entirely so every match is reported
|
||||
## Understanding `--confidence`
|
||||
|
||||
The `--confidence` flag sets a minimum confidence threshold, not an exact match.
|
||||
|
|
@ -972,7 +975,22 @@ The `--confidence` flag sets a minimum confidence threshold, not an exact match.
|
|||
|
||||
### Ignore known false positives
|
||||
|
||||
Use `--skip-regex` and `--skip-word` to suppress findings you know are benign. Both flags may be provided multiple times and are tested against the secret value **and** the full match context.
|
||||
Use `--skip-regex` and `--skip-word` to suppress findings you know are benign. Both flags may be provided multiple times and are tested against the secret value **and** the full match context.
|
||||
|
||||
### Inline ignore directives
|
||||
|
||||
Add `kingfisher:ignore` (or `kingfisher:allow`) anywhere on the same line as a finding to silence it. Multi-line strings may also be ignored by placing the directive on the closing delimiter line, on the next logical line after the string, **or** on a comment immediately before the value:
|
||||
|
||||
```python
|
||||
# kingfisher:ignore
|
||||
API_KEY = """
|
||||
line 1
|
||||
line 2
|
||||
"""
|
||||
# kingfisher:ignore
|
||||
```
|
||||
|
||||
Kingfisher searches the surrounding lines for these tokens without requiring language-specific comment markers, so directives work even in templated files or unusual syntaxes. To reuse existing inline directives from other scanners, pass `--compat-ignore-comments` to also accept `gitleaks:allow` and `trufflehog:ignore`. Use `--no-ignore` when you want to disable inline suppressions entirely.
|
||||
|
||||
With `--skip-regex`, these should be Rust compatible regular expressions, which you can test out at [regex101](https://regex101.com)
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,14 @@ pub struct ScanArgs {
|
|||
/// Skipwords to allow-list secret matches (case-insensitive, repeatable)
|
||||
#[arg(long = "skip-word", value_name = "WORD")]
|
||||
pub skip_word: Vec<String>,
|
||||
|
||||
/// Also recognise `gitleaks:allow` and `trufflehog:ignore` inline directives
|
||||
#[arg(long = "compat-ignore-comments", default_value_t = false)]
|
||||
pub compat_ignore_comments: bool,
|
||||
|
||||
/// Disable inline ignore directives entirely
|
||||
#[arg(long = "no-ignore", default_value_t = false)]
|
||||
pub no_inline_ignore: bool,
|
||||
}
|
||||
|
||||
/// Confidence levels for findings
|
||||
|
|
|
|||
285
src/inline_ignore.rs
Normal file
285
src/inline_ignore.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
use crate::location::OffsetSpan;
|
||||
|
||||
/// Configuration for inline ignore directives.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct InlineIgnoreConfig {
|
||||
tokens: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl InlineIgnoreConfig {
|
||||
/// Create a new configuration.
|
||||
///
|
||||
/// * `include_external_syntax` - when true, also recognise the comment
|
||||
/// directives used by other scanners such as Gitleaks and Trufflehog.
|
||||
pub fn new(include_external_syntax: bool) -> Self {
|
||||
let mut tokens = vec!["kingfisher:ignore", "kingfisher:allow"];
|
||||
if include_external_syntax {
|
||||
tokens.extend(["gitleaks:allow", "trufflehog:ignore"]);
|
||||
}
|
||||
Self { tokens }
|
||||
}
|
||||
|
||||
/// Return a configuration with inline ignores disabled.
|
||||
pub fn disabled() -> Self {
|
||||
Self { tokens: Vec::new() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_tokens(&self) -> bool {
|
||||
!self.tokens.is_empty()
|
||||
}
|
||||
|
||||
/// Returns `true` when the provided blob slice contains an inline ignore
|
||||
/// directive that should suppress a finding for the given span.
|
||||
pub fn should_ignore(&self, blob_bytes: &[u8], span: &OffsetSpan) -> bool {
|
||||
if !self.has_tokens() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (start_line_start, start_line_end) = line_bounds(blob_bytes, span.start);
|
||||
if start_line_end > start_line_start {
|
||||
let start_line = &blob_bytes[start_line_start..start_line_end];
|
||||
if line_has_directive(start_line, &self.tokens) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Scan backwards to allow directives that appear before the start of a
|
||||
// multi-line string or value. This mirrors tools like Gitleaks where
|
||||
// the ignore directive is often placed immediately above the secret.
|
||||
let mut cursor = start_line_start;
|
||||
while cursor > 0 {
|
||||
let previous_index = cursor.saturating_sub(1);
|
||||
let (prev_start, prev_end) = line_bounds(blob_bytes, previous_index);
|
||||
if prev_end <= prev_start {
|
||||
break;
|
||||
}
|
||||
|
||||
let prev_line = &blob_bytes[prev_start..prev_end];
|
||||
if line_has_directive(prev_line, &self.tokens) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !should_skip_for_directive_search(prev_line) {
|
||||
break;
|
||||
}
|
||||
|
||||
if prev_start == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = prev_start;
|
||||
}
|
||||
|
||||
let end_index = if span.end == 0 { 0 } else { span.end - 1 };
|
||||
let (closing_line_start, closing_line_end) =
|
||||
line_bounds(blob_bytes, end_index.min(blob_bytes.len()));
|
||||
if closing_line_end > closing_line_start
|
||||
&& (closing_line_start != start_line_start || closing_line_end != start_line_end)
|
||||
{
|
||||
let closing_line = &blob_bytes[closing_line_start..closing_line_end];
|
||||
if line_has_directive(closing_line, &self.tokens) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also consider lines after the match so that multi-line strings can be
|
||||
// ignored when the directive appears after the closing delimiter (a
|
||||
// common pattern in languages like Python).
|
||||
let mut cursor = closing_line_end;
|
||||
while cursor < blob_bytes.len() {
|
||||
if blob_bytes[cursor] == b'\n' {
|
||||
cursor += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (_, next_end) = line_bounds(blob_bytes, cursor);
|
||||
if next_end <= cursor {
|
||||
break;
|
||||
}
|
||||
|
||||
let next_line = &blob_bytes[cursor..next_end];
|
||||
if line_has_directive(next_line, &self.tokens) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !should_skip_for_directive_search(next_line) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = next_end;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn should_skip_for_directive_search(line: &[u8]) -> bool {
|
||||
let trimmed = trim_ascii_whitespace(line);
|
||||
if trimmed.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if trimmed.iter().all(|&b| b == trimmed[0]) && matches!(trimmed[0], b'"' | b'\'' | b'`') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ends_with_multiline_delimiter(trimmed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn ends_with_multiline_delimiter(trimmed: &[u8]) -> bool {
|
||||
if trimmed.len() < 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let last = *trimmed.last().unwrap();
|
||||
if !matches!(last, b'"' | b'\'' | b'`') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let count = trimmed.iter().rev().take_while(|&&ch| ch == last).count();
|
||||
|
||||
count >= 3
|
||||
}
|
||||
|
||||
fn trim_ascii_whitespace(line: &[u8]) -> &[u8] {
|
||||
let mut start = 0;
|
||||
while start < line.len() && line[start].is_ascii_whitespace() {
|
||||
start += 1;
|
||||
}
|
||||
|
||||
let mut end = line.len();
|
||||
while end > start && line[end - 1].is_ascii_whitespace() {
|
||||
end -= 1;
|
||||
}
|
||||
|
||||
&line[start..end]
|
||||
}
|
||||
|
||||
fn line_bounds(bytes: &[u8], index: usize) -> (usize, usize) {
|
||||
if bytes.is_empty() {
|
||||
return (0, 0);
|
||||
}
|
||||
let mut start = index.min(bytes.len());
|
||||
while start > 0 && bytes[start - 1] != b'\n' {
|
||||
start -= 1;
|
||||
}
|
||||
let mut end = index.min(bytes.len());
|
||||
while end < bytes.len() && bytes[end] != b'\n' {
|
||||
end += 1;
|
||||
}
|
||||
(start, end)
|
||||
}
|
||||
|
||||
fn line_has_directive(line: &[u8], tokens: &[&'static str]) -> bool {
|
||||
if line.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut lowercase = line.to_vec();
|
||||
lowercase.iter_mut().for_each(|b| *b = b.to_ascii_lowercase());
|
||||
|
||||
tokens.iter().any(|token| memchr::memmem::find(&lowercase, token.as_bytes()).is_some())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
line_bounds, line_has_directive, should_skip_for_directive_search, trim_ascii_whitespace,
|
||||
InlineIgnoreConfig,
|
||||
};
|
||||
use crate::location::OffsetSpan;
|
||||
|
||||
#[test]
|
||||
fn bounds_cover_expected_ranges() {
|
||||
let data = b"one\ntwo\nthree";
|
||||
assert_eq!(line_bounds(data, 0), (0, 3));
|
||||
assert_eq!(line_bounds(data, 4), (4, 7));
|
||||
assert_eq!(line_bounds(data, data.len()), (8, 13));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_directives_in_lines() {
|
||||
let tokens = ["kingfisher:ignore", "kingfisher:allow"];
|
||||
assert!(line_has_directive(b"secret # kingfisher:ignore", &tokens));
|
||||
assert!(line_has_directive(b"kingfisher:allow before value", &tokens));
|
||||
assert!(line_has_directive(b"value // TruffleHog:Ignore", &["trufflehog:ignore"]));
|
||||
assert!(!line_has_directive(b"secret", &tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn respects_multiline_block_comment_prefix() {
|
||||
let tokens = ["kingfisher:ignore"];
|
||||
assert!(line_has_directive(b" * kingfisher:ignore", &tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_multi_line_string_with_trailing_comment() {
|
||||
let blob = b"let secret = \"\"\"\nline1\nline2\n\"\"\"\n# kingfisher:ignore\n";
|
||||
let matched = b"line1\nline2\n";
|
||||
let start = blob
|
||||
.windows(matched.len())
|
||||
.position(|window| window == matched)
|
||||
.expect("match bytes present");
|
||||
let span = OffsetSpan::from_range(start..start + matched.len());
|
||||
let config = InlineIgnoreConfig::new(false);
|
||||
assert!(config.should_ignore(blob, &span));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_multiline_without_trailing_newline() {
|
||||
let blob = b"let secret = \"\"\"\nline1\nline2\n\"\"\"\n# kingfisher:ignore\n";
|
||||
let matched = b"line1\nline2";
|
||||
let start = blob
|
||||
.windows(matched.len())
|
||||
.position(|window| window == matched)
|
||||
.expect("match bytes present");
|
||||
let span = OffsetSpan::from_range(start..start + matched.len());
|
||||
let config = InlineIgnoreConfig::new(false);
|
||||
assert!(config.should_ignore(blob, &span));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_multiline_with_directive_before_secret() {
|
||||
let blob = b"// kingfisher:ignore\nlet secret = \"\"\"\nline1\nline2\n\"\"\"\n";
|
||||
let matched = b"line1\nline2\n";
|
||||
let start = blob
|
||||
.windows(matched.len())
|
||||
.position(|window| window == matched)
|
||||
.expect("match bytes present");
|
||||
let span = OffsetSpan::from_range(start..start + matched.len());
|
||||
let config = InlineIgnoreConfig::new(false);
|
||||
assert!(config.should_ignore(blob, &span));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_ascii_whitespace_returns_inner_slice() {
|
||||
assert_eq!(trim_ascii_whitespace(b" abc "), b"abc");
|
||||
assert!(trim_ascii_whitespace(b" ").is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_lines_with_only_delimiters() {
|
||||
assert!(should_skip_for_directive_search(b"\"\"\""));
|
||||
assert!(should_skip_for_directive_search(b" \"\"\" "));
|
||||
assert!(should_skip_for_directive_search(b"let secret = \"\"\""));
|
||||
assert!(!should_skip_for_directive_search(b"value"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_config_never_ignores() {
|
||||
let blob = b"let secret = 'value' # kingfisher:ignore";
|
||||
let matched = b"value";
|
||||
let start = blob
|
||||
.windows(matched.len())
|
||||
.position(|window| window == matched)
|
||||
.expect("match bytes present");
|
||||
let span = OffsetSpan::from_range(start..start + matched.len());
|
||||
let config = InlineIgnoreConfig::disabled();
|
||||
assert!(!config.should_ignore(blob, &span));
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ pub mod git_url;
|
|||
pub mod gitea;
|
||||
pub mod github;
|
||||
pub mod gitlab;
|
||||
pub mod inline_ignore;
|
||||
pub mod jira;
|
||||
pub mod liquid_filters;
|
||||
pub mod location;
|
||||
|
|
|
|||
|
|
@ -437,6 +437,8 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
|||
skip_word: Vec::new(),
|
||||
output_args: OutputArgs { output: None, format: ReportOutputFormat::Pretty },
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
}
|
||||
}
|
||||
/// Run the rules check command
|
||||
|
|
|
|||
158
src/matcher.rs
158
src/matcher.rs
|
|
@ -23,6 +23,7 @@ use xxhash_rust::xxh3::xxh3_64;
|
|||
use crate::{
|
||||
blob::{Blob, BlobId, BlobIdMap},
|
||||
entropy::calculate_shannon_entropy,
|
||||
inline_ignore::InlineIgnoreConfig,
|
||||
location::{Location, LocationMapping, OffsetSpan, SourcePoint, SourceSpan},
|
||||
origin::OriginSet,
|
||||
parser,
|
||||
|
|
@ -199,6 +200,9 @@ pub struct Matcher<'a> {
|
|||
|
||||
/// Rule profiler for measuring performance of individual rules
|
||||
profiler: Option<Arc<ConcurrentRuleProfiler>>,
|
||||
|
||||
/// Configuration that controls inline ignore directives
|
||||
inline_ignore_config: InlineIgnoreConfig,
|
||||
}
|
||||
/// This `Drop` implementation updates the `global_stats` with the local stats
|
||||
impl<'a> Drop for Matcher<'a> {
|
||||
|
|
@ -226,6 +230,8 @@ impl<'a> Matcher<'a> {
|
|||
global_stats: Option<&'a Mutex<MatcherStats>>,
|
||||
enable_profiling: bool,
|
||||
shared_profiler: Option<Arc<ConcurrentRuleProfiler>>,
|
||||
include_external_ignore_syntax: bool,
|
||||
disable_inline_ignores: bool,
|
||||
) -> Result<Self> {
|
||||
// Changed: removed `with_capacity(16384)` so we don't pre-allocate a large Vec
|
||||
let raw_matches_scratch = Vec::new();
|
||||
|
|
@ -247,6 +253,11 @@ impl<'a> Matcher<'a> {
|
|||
seen_blobs,
|
||||
user_data,
|
||||
profiler,
|
||||
inline_ignore_config: if disable_inline_ignores {
|
||||
InlineIgnoreConfig::disabled()
|
||||
} else {
|
||||
InlineIgnoreConfig::new(include_external_ignore_syntax)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -403,6 +414,7 @@ impl<'a> Matcher<'a> {
|
|||
redact,
|
||||
&filename,
|
||||
self.profiler.as_ref(),
|
||||
&self.inline_ignore_config,
|
||||
);
|
||||
}
|
||||
// If tree-sitter produced base64-decoded matches, try them against all rules
|
||||
|
|
@ -427,6 +439,7 @@ impl<'a> Matcher<'a> {
|
|||
redact,
|
||||
&filename,
|
||||
self.profiler.as_ref(),
|
||||
&self.inline_ignore_config,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -457,6 +470,7 @@ impl<'a> Matcher<'a> {
|
|||
redact,
|
||||
&filename,
|
||||
self.profiler.as_ref(),
|
||||
&self.inline_ignore_config,
|
||||
);
|
||||
}
|
||||
if depth + 1 < MAX_B64_DEPTH {
|
||||
|
|
@ -560,6 +574,7 @@ fn filter_match<'b>(
|
|||
redact: bool,
|
||||
filename: &str,
|
||||
profiler: Option<&Arc<ConcurrentRuleProfiler>>,
|
||||
inline_ignore_config: &InlineIgnoreConfig,
|
||||
) {
|
||||
let mut timer =
|
||||
profiler.map(|p| RuleTimer::new(p, rule.id(), rule.name(), &rule.syntax.pattern, filename));
|
||||
|
|
@ -590,6 +605,10 @@ fn filter_match<'b>(
|
|||
let matching_input_offset_span = OffsetSpan::from_range(
|
||||
(start + matching_input.start())..(start + matching_input.end()),
|
||||
);
|
||||
if inline_ignore_config.should_ignore(blob_bytes, &matching_input_offset_span) {
|
||||
debug!("Skipping match due to inline ignore directive");
|
||||
continue;
|
||||
}
|
||||
let match_key = compute_match_key(
|
||||
matching_input.as_bytes(),
|
||||
rule.id().as_bytes(),
|
||||
|
|
@ -961,7 +980,7 @@ pub fn compute_finding_fingerprint(
|
|||
// -------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
// ---------------------------------------------------------------------
|
||||
|
|
@ -970,7 +989,11 @@ mod test {
|
|||
use proptest::prelude::*;
|
||||
|
||||
use super::*;
|
||||
use crate::rules::rule::{DependsOnRule, HttpRequest, HttpValidation, RuleSyntax, Validation};
|
||||
use crate::{
|
||||
blob::{Blob, BlobIdMap},
|
||||
origin::{Origin, OriginSet},
|
||||
rules::rule::{DependsOnRule, HttpRequest, HttpValidation, RuleSyntax, Validation},
|
||||
};
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
|
|
@ -1009,7 +1032,17 @@ mod test {
|
|||
let rules_db = RulesDatabase::from_rules(vec![rule]).unwrap();
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut m = Matcher::new(&rules_db, scanner_pool, &seen, None, false, None).unwrap();
|
||||
let mut m = Matcher::new(
|
||||
&rules_db,
|
||||
scanner_pool,
|
||||
&seen,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// ── run the scan ──────────────────────────────────────────────
|
||||
m.scan_bytes_raw(&noise, "buf").unwrap();
|
||||
|
|
@ -1080,6 +1113,8 @@ mod test {
|
|||
None,
|
||||
enable_rule_profiling,
|
||||
None, // Pass the shared profiler
|
||||
false,
|
||||
false,
|
||||
)?;
|
||||
matcher.scan_bytes_raw(input.as_bytes(), "fname")?;
|
||||
assert_eq!(
|
||||
|
|
@ -1167,7 +1202,7 @@ mod test {
|
|||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut m = Matcher::new(&rules_db, scanner_pool, &seen, None, false, None)?;
|
||||
let mut m = Matcher::new(&rules_db, scanner_pool, &seen, None, false, None, false, false)?;
|
||||
|
||||
let buf = b"dup dup"; // two literal hits, same rule
|
||||
|
||||
|
|
@ -1184,4 +1219,119 @@ mod test {
|
|||
assert_eq!(second_len, 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_comment_skips_match() -> Result<()> {
|
||||
let rule = Rule::new(RuleSyntax {
|
||||
id: "inline.ignore".into(),
|
||||
name: "inline".into(),
|
||||
pattern: "secret_token".into(),
|
||||
confidence: crate::rules::rule::Confidence::Low,
|
||||
min_entropy: 0.0,
|
||||
visible: true,
|
||||
examples: vec![],
|
||||
negative_examples: vec![],
|
||||
references: vec![],
|
||||
validation: None::<Validation>,
|
||||
depends_on_rule: vec![],
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut matcher =
|
||||
Matcher::new(&rules_db, scanner_pool, &seen, None, false, None, false, false)?;
|
||||
|
||||
let blob = Blob::from_bytes(b"let key = \"secret_token\" # kingfisher:ignore".to_vec());
|
||||
let origin = OriginSet::from(Origin::from_file(PathBuf::from("inline.txt")));
|
||||
|
||||
match matcher.scan_blob(&blob, &origin, None, false, false, false)? {
|
||||
ScanResult::New(matches) => assert!(matches.is_empty()),
|
||||
_ => panic!("unexpected scan result"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_comment_after_multiline_secret_skips_match() -> Result<()> {
|
||||
let rule = Rule::new(RuleSyntax {
|
||||
id: "inline.multiline".into(),
|
||||
name: "inline multiline".into(),
|
||||
pattern: "line1\\s+line2".into(),
|
||||
confidence: crate::rules::rule::Confidence::Low,
|
||||
min_entropy: 0.0,
|
||||
visible: true,
|
||||
examples: vec![],
|
||||
negative_examples: vec![],
|
||||
references: vec![],
|
||||
validation: None::<Validation>,
|
||||
depends_on_rule: vec![],
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut matcher =
|
||||
Matcher::new(&rules_db, scanner_pool, &seen, None, false, None, false, false)?;
|
||||
|
||||
let blob = Blob::from_bytes(
|
||||
br#"let data = """
|
||||
line1
|
||||
line2
|
||||
"""
|
||||
# kingfisher:ignore
|
||||
"#
|
||||
.to_vec(),
|
||||
);
|
||||
let origin = OriginSet::from(Origin::from_file(PathBuf::from("multiline.txt")));
|
||||
|
||||
match matcher.scan_blob(&blob, &origin, None, false, false, false)? {
|
||||
ScanResult::New(matches) => assert!(matches.is_empty()),
|
||||
_ => panic!("unexpected scan result"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_flag_controls_external_directives() -> Result<()> {
|
||||
let rule = Rule::new(RuleSyntax {
|
||||
id: "inline.compat".into(),
|
||||
name: "inline compat".into(),
|
||||
pattern: "supersecret123".into(),
|
||||
confidence: crate::rules::rule::Confidence::Low,
|
||||
min_entropy: 0.0,
|
||||
visible: true,
|
||||
examples: vec![],
|
||||
negative_examples: vec![],
|
||||
references: vec![],
|
||||
validation: None::<Validation>,
|
||||
depends_on_rule: vec![],
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
|
||||
let blob = Blob::from_bytes(b"token = \"supersecret123\" # gitleaks:allow".to_vec());
|
||||
let origin = OriginSet::from(Origin::from_file(PathBuf::from("compat.txt")));
|
||||
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut matcher =
|
||||
Matcher::new(&rules_db, scanner_pool, &seen, None, false, None, false, false)?;
|
||||
let matches_without_compat =
|
||||
match matcher.scan_blob(&blob, &origin, None, false, false, false)? {
|
||||
ScanResult::New(matches) => matches.len(),
|
||||
_ => panic!("unexpected scan result"),
|
||||
};
|
||||
assert_eq!(matches_without_compat, 1, "directive should be ignored without compat flag");
|
||||
|
||||
let seen = BlobIdMap::new();
|
||||
let scanner_pool = Arc::new(ScannerPool::new(Arc::new(rules_db.vsdb.clone())));
|
||||
let mut matcher =
|
||||
Matcher::new(&rules_db, scanner_pool, &seen, None, false, None, true, false)?;
|
||||
match matcher.scan_blob(&blob, &origin, None, false, false, false)? {
|
||||
ScanResult::New(matches) => assert!(matches.is_empty()),
|
||||
_ => panic!("unexpected scan result"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -863,6 +863,8 @@ mod tests {
|
|||
manage_baseline: false,
|
||||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let record = reporter.build_finding_record(&report_match, &scan_args);
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ mod tests {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,8 @@ pub fn enumerate_filesystem_inputs(
|
|||
Some(&matcher_stats),
|
||||
enable_profiling,
|
||||
Some(shared_profiler),
|
||||
args.compat_ignore_comments,
|
||||
args.no_inline_ignore,
|
||||
)?;
|
||||
let blob_processor_init_time = Mutex::new(t1.elapsed());
|
||||
let make_blob_processor = || -> BlobProcessor {
|
||||
|
|
|
|||
|
|
@ -622,6 +622,8 @@ pub async fn fetch_s3_objects(
|
|||
Some(matcher_stats),
|
||||
enable_profiling,
|
||||
Some(shared_profiler.clone()),
|
||||
args.compat_ignore_comments,
|
||||
args.no_inline_ignore,
|
||||
)?;
|
||||
let mut processor = BlobProcessor { matcher };
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ fn run_skiplist(skip_regex: Vec<String>, skip_skipword: Vec<String>) -> Result<u
|
|||
skip_regex: skip_regex,
|
||||
skip_word: skip_skipword,
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -132,6 +132,8 @@ fn test_bitbucket_remote_scan() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ rules:
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -139,6 +139,8 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
// Create global arguments
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -137,6 +137,8 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
@ -272,6 +274,8 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,8 @@ impl TestContext {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules).load(&scan_args)?;
|
||||
|
|
@ -246,6 +248,8 @@ async fn test_scan_slack_messages() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
|
|
@ -195,6 +195,8 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
/* --------------------------------------------------------- *
|
||||
|
|
|
|||
|
|
@ -138,6 +138,8 @@ impl TestContext {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules)
|
||||
|
|
@ -248,6 +250,8 @@ impl TestContext {
|
|||
skip_regex: Vec::new(),
|
||||
skip_word: Vec::new(),
|
||||
no_base64: false,
|
||||
compat_ignore_comments: false,
|
||||
no_inline_ignore: false,
|
||||
};
|
||||
|
||||
let global_args = GlobalArgs {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue