Date: Tue, 23 Sep 2025 16:21:17 -0700
Subject: [PATCH 04/12] Updated README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 874c595..8419991 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ Kingfisher is a blazingly fast secret‑scanning and live validation tool built
Originally forked from Praetorian’s Nosey Parker, Kingfisher **adds** live cloud-API validation; many more targets (GitLab, BitBucket, Gitea, S3, Docker, Jira, Confluence, Slack); compressed-file extraction and scanning; baseline and allowlist controls; language-aware detection (~20 languages); and a native Windows binary. See [Origins and Divergence](#origins-and-divergence) for details.
## Key Features
-- **Multiple Scan Targets**:
+- **Multiple Scan Targets**:
From 74b7626f4ded290794ece07d3c96a5fb3b611acd Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 16:23:12 -0700
Subject: [PATCH 05/12] Updated README
---
README.md | 29 ++++++++++++++++-------------
1 file changed, 16 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 8419991..df73c9b 100644
--- a/README.md
+++ b/README.md
@@ -11,19 +11,22 @@ Kingfisher is a blazingly fast secret‑scanning and live validation tool built
Originally forked from Praetorian’s Nosey Parker, Kingfisher **adds** live cloud-API validation; many more targets (GitLab, BitBucket, Gitea, S3, Docker, Jira, Confluence, Slack); compressed-file extraction and scanning; baseline and allowlist controls; language-aware detection (~20 languages); and a native Windows binary. See [Origins and Divergence](#origins-and-divergence) for details.
## Key Features
-- **Multiple Scan Targets**:
-
-
-
-
-
-
-
-
-
-
-
-
+- **Multiple Scan Targets**:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- **Performance**: multithreaded, Hyperscan‑powered scanning built for huge codebases
- **Extensible rules**: hundreds of built-in detectors plus YAML-defined custom rules ([docs/RULES.md](/docs/RULES.md))
From dbf921937d7a670bf5a873df746bcec0267f3434 Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 16:27:53 -0700
Subject: [PATCH 06/12] Updated README
---
README.md | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index df73c9b..831d582 100644
--- a/README.md
+++ b/README.md
@@ -30,9 +30,8 @@ Originally forked from Praetorian’s Nosey Parker, Kingfisher **adds** live clo
- **Performance**: multithreaded, Hyperscan‑powered scanning built for huge codebases
- **Extensible rules**: hundreds of built-in detectors plus YAML-defined custom rules ([docs/RULES.md](/docs/RULES.md))
- - **Broad AI SaaS coverage**: finds and validates tokens for OpenAI, Anthropic, Google Gemini, Cohere, Mistral, Stability AI, Replicate, xAI (Grok), Ollama, Langchain, Perplexity, Weights & Biases, Cerebras, Friendli, Fireworks.ai, NVIDIA NIM, Together.ai, Zhipu, and many more
+- **Broad AI SaaS coverage**: finds and validates tokens for OpenAI, Anthropic, Google Gemini, Cohere, Mistral, Stability AI, Replicate, xAI (Grok), Ollama, Langchain, Perplexity, Weights & Biases, Cerebras, Friendli, Fireworks.ai, NVIDIA NIM, Together.ai, Zhipu, and many more
- **Compressed Files**: Supports extracting and scanning compressed files for secrets
-- Decode Base64 blobs and scan their contents for secrets while skipping short strings for performance. This has a small performance impact and can be disabled with `--no-base64`
- **Baseline management**: generate and track baselines to suppress known secrets ([docs/BASELINE.md](/docs/BASELINE.md))
**Learn more:** [Introducing Kingfisher: Real‑Time Secret Detection and Validation](https://www.mongodb.com/blog/post/product-release-announcements/introducing-kingfisher-real-time-secret-detection-validation)
@@ -908,6 +907,7 @@ leaves the default unchanged.
## Notable Scan Options
- `--no-dedup`: Report every occurrence of a finding (disable the default de-duplicate behavior)
+- `--no-base64`: By default, Kingfisher finds and decodes base64 blobs and scans them for secrets. This adds a slight performance overhead; use this flag to disable
- `--confidence `: (low|medium|high)
- `--min-entropy `: Override default threshold
- `--no-binary`: Skip binary files
@@ -919,7 +919,6 @@ leaves the default unchanged.
- `--manage-baseline`: Create or update the baseline file with current findings
- `--skip-regex `: Ignore findings whose text matches this regex (repeatable)
- `--skip-word `: Ignore findings containing this case-insensitive word (repeatable)
-
## Understanding `--confidence`
The `--confidence` flag sets a minimum confidence threshold, not an exact match.
From f4505b94ab94fbc6b77b7cf40f52391e7b397309 Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 16:29:13 -0700
Subject: [PATCH 07/12] Updated README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 831d582..ec89f63 100644
--- a/README.md
+++ b/README.md
@@ -975,7 +975,7 @@ Since that initial fork, it has diverged heavily from Nosey Parker:
- Collapsed the workflow into a single scan-and-report phase with direct JSON/BSON/SARIF outputs
- Added Tree-Sitter parsing on top of Hyperscan for deeper language-aware detection
- Removed datastore-driven reporting/annotations in favor of live validation, baselines, allowlists, and compressed-file extraction
-- Expanded support for new targets (GitLab, Jira, Confluence, Slack, S3, Docker, etc.)
+- Expanded support for new targets (GitLab, BitBucket, Gitea, Jira, Confluence, Slack, S3, Docker, etc.)
- Delivered cross-platform builds, including native Windows
From e82f9ace8470d62ce4b05691e727ad5ea0139fb9 Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 16:39:47 -0700
Subject: [PATCH 08/12] Updated README
---
src/reporter/styles.rs | 16 +++-------------
1 file changed, 3 insertions(+), 13 deletions(-)
diff --git a/src/reporter/styles.rs b/src/reporter/styles.rs
index 9f96c9c..56b32b1 100644
--- a/src/reporter/styles.rs
+++ b/src/reporter/styles.rs
@@ -11,16 +11,14 @@ pub struct Styles {
pub style_active_creds: Style,
pub style_match: Style,
pub style_metadata: Style,
- is_term: bool,
}
impl Styles {
pub fn new(use_color: bool) -> Self {
- let stdout_is_tty = std::io::stdout().is_terminal();
- let is_term = Term::stdout().is_term();
+ // Trust the `use_color` decision from the caller.
+ let styles_enabled = use_color;
- // Enable color only when explicitly requested and stdout is a terminal.
- let styles_enabled = use_color && stdout_is_tty && is_term;
let style_finding_heading = Style::new().bright().white().force_styling(styles_enabled);
+
let style_finding_active_heading =
Style::new().bold().bright().cyan().force_styling(styles_enabled);
let style_rule = Style::new().bright().bold().blue().force_styling(styles_enabled);
@@ -36,14 +34,6 @@ impl Styles {
style_match,
style_metadata,
style_active_creds,
- is_term,
}
}
- // pub fn apply>(&self, text: T, style: &Style) -> String {
- // if self.is_term {
- // style.apply_to(text.as_ref()).to_string()
- // } else {
- // text.as_ref().to_string()
- // }
- // }
}
From ea24d9a0d5a0144e9843adfa729c80f25d67648e Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 16:41:04 -0700
Subject: [PATCH 09/12] Updated README
---
src/reporter/styles.rs | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/reporter/styles.rs b/src/reporter/styles.rs
index 56b32b1..0a16ac4 100644
--- a/src/reporter/styles.rs
+++ b/src/reporter/styles.rs
@@ -1,5 +1,3 @@
-use std::io::IsTerminal;
-
pub use console::{Style, StyledObject, Term};
#[allow(dead_code)]
From 08b87eadf462999ae093c4cb373f08828b454d95 Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Tue, 23 Sep 2025 17:24:11 -0700
Subject: [PATCH 10/12] Populate the finding path from git blob metadata so
history-derived secrets display their file location instead of an empty path
---
CHANGELOG.md | 1 +
src/reporter.rs | 188 +++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 187 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 488a7c2..2fb02ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
## [v1.54.0]
- Added first-class Gitea support, including CLI commands, environment-based authentication, documentation, and integration with scans and repository enumeration.
+- Populate the finding path from git blob metadata so history-derived secrets display their file location instead of an empty path
## [v1.53.0]
- Added first-class Bitbucket support, including CLI commands, authentication helpers, documentation, and integration testing.
diff --git a/src/reporter.rs b/src/reporter.rs
index bc4bd86..eccdcf1 100644
--- a/src/reporter.rs
+++ b/src/reporter.rs
@@ -449,10 +449,15 @@ impl DetailsReporter {
Some(e.path.display().to_string())
}
}
+ Origin::GitRepo(e) => e.first_commit.as_ref().map(|c| c.blob_path.clone()),
Origin::Extended(e) => e.path().map(|p| p.display().to_string()),
- _ => None,
})
- .unwrap_or_default();
+ .unwrap_or_else(|| {
+ rm.origin
+ .iter()
+ .find_map(|origin| origin.blob_path().map(|p| p.display().to_string()))
+ .unwrap_or_default()
+ });
FindingReporterRecord {
rule: RuleMetadata {
@@ -617,6 +622,185 @@ pub struct FindingRecordData {
pub git_metadata: Option,
}
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ blob::{BlobId, BlobMetadata},
+ cli::commands::inputs::{ContentFilteringArgs, InputSpecifierArgs},
+ cli::commands::output::OutputArgs,
+ cli::commands::scan::{ConfidenceLevel, ScanArgs},
+ cli::commands::{
+ bitbucket::{BitbucketAuthArgs, BitbucketRepoType},
+ github::{GitCloneMode, GitHistoryMode, GitHubRepoType},
+ gitlab::GitLabRepoType,
+ rules::RuleSpecifierArgs,
+ },
+ location::{Location, OffsetSpan, SourcePoint, SourceSpan},
+ matcher::{SerializableCapture, SerializableCaptures},
+ origin::OriginSet,
+ rules::rule::{Confidence, Rule, RuleSyntax},
+ };
+ use gix::{date::Time, ObjectId};
+ use smallvec::SmallVec;
+ use std::path::PathBuf;
+ use tempfile::tempdir;
+
+ #[test]
+ fn build_finding_record_uses_git_blob_path() {
+ let temp = tempdir().unwrap();
+ let datastore =
+ Arc::new(Mutex::new(findings_store::FindingsStore::new(temp.path().to_path_buf())));
+ let reporter = DetailsReporter { datastore, styles: Styles::new(false), only_valid: false };
+
+ let repo_path = Arc::new(PathBuf::from("/tmp/repo"));
+ let commit_metadata = Arc::new(CommitMetadata {
+ commit_id: ObjectId::from_hex(b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(),
+ committer_name: "Alice".into(),
+ committer_email: "alice@example.com".into(),
+ committer_timestamp: Time::new(0, 0),
+ });
+ let blob_path = "path/in/history.txt".to_string();
+ let origin = OriginSet::new(
+ Origin::from_git_repo_with_first_commit(repo_path, commit_metadata, blob_path.clone()),
+ vec![],
+ );
+
+ let rule = Arc::new(Rule::new(RuleSyntax {
+ name: "Test Rule".into(),
+ id: "test.rule".into(),
+ pattern: ".*".into(),
+ min_entropy: 0.0,
+ confidence: Confidence::Medium,
+ visible: true,
+ examples: vec![],
+ negative_examples: vec![],
+ references: vec![],
+ validation: None,
+ depends_on_rule: vec![],
+ }));
+
+ let blob_id = BlobId::new(b"blob-data");
+ let report_match = ReportMatch {
+ origin,
+ blob_metadata: BlobMetadata {
+ id: blob_id,
+ num_bytes: 42,
+ mime_essence: None,
+ language: Some("Unknown".into()),
+ },
+ m: Match {
+ location: Location {
+ offset_span: OffsetSpan { start: 0, end: 10 },
+ source_span: SourceSpan {
+ start: SourcePoint { line: 19, column: 0 },
+ end: SourcePoint { line: 19, column: 10 },
+ },
+ },
+ groups: SerializableCaptures {
+ captures: SmallVec::<[SerializableCapture; 2]>::new(),
+ },
+ blob_id,
+ finding_fingerprint: 123,
+ rule: Arc::clone(&rule),
+ validation_response_body: "Bad credentials".into(),
+ validation_response_status: 401,
+ validation_success: false,
+ calculated_entropy: 5.29,
+ visible: true,
+ is_base64: false,
+ },
+ comment: None,
+ match_confidence: Confidence::Medium,
+ visible: true,
+ validation_response_body: "Bad credentials".into(),
+ validation_response_status: 401,
+ validation_success: false,
+ };
+
+ let scan_args = ScanArgs {
+ num_jobs: 1,
+ rules: RuleSpecifierArgs::default(),
+ input_specifier_args: InputSpecifierArgs {
+ path_inputs: Vec::new(),
+ git_url: Vec::new(),
+ github_user: Vec::new(),
+ github_organization: Vec::new(),
+ github_exclude: Vec::new(),
+ all_github_organizations: false,
+ github_api_url: Url::parse("https://api.github.com/").unwrap(),
+ github_repo_type: GitHubRepoType::Source,
+ gitlab_user: Vec::new(),
+ gitlab_group: Vec::new(),
+ gitlab_exclude: Vec::new(),
+ all_gitlab_groups: false,
+ gitlab_api_url: Url::parse("https://gitlab.com/").unwrap(),
+ gitlab_repo_type: GitLabRepoType::All,
+ gitlab_include_subgroups: false,
+ bitbucket_user: Vec::new(),
+ bitbucket_workspace: Vec::new(),
+ bitbucket_project: Vec::new(),
+ bitbucket_exclude: Vec::new(),
+ all_bitbucket_workspaces: false,
+ bitbucket_api_url: Url::parse("https://api.bitbucket.org/2.0/").unwrap(),
+ bitbucket_repo_type: BitbucketRepoType::Source,
+ bitbucket_auth: BitbucketAuthArgs::default(),
+ jira_url: None,
+ jql: None,
+ confluence_url: None,
+ cql: None,
+ slack_query: None,
+ slack_api_url: Url::parse("https://slack.com/api/").unwrap(),
+ max_results: 100,
+ s3_bucket: None,
+ s3_prefix: None,
+ role_arn: None,
+ aws_local_profile: None,
+ docker_image: Vec::new(),
+ git_clone: GitCloneMode::Bare,
+ git_history: GitHistoryMode::Full,
+ commit_metadata: true,
+ repo_artifacts: false,
+ scan_nested_repos: true,
+ since_commit: None,
+ branch: None,
+ },
+ content_filtering_args: ContentFilteringArgs {
+ max_file_size_mb: 256.0,
+ exclude: Vec::new(),
+ no_extract_archives: false,
+ extraction_depth: 2,
+ no_binary: false,
+ },
+ confidence: ConfidenceLevel::Medium,
+ no_validate: false,
+ only_valid: false,
+ min_entropy: None,
+ rule_stats: false,
+ no_dedup: false,
+ redact: false,
+ git_repo_timeout: 1_800,
+ output_args: OutputArgs { output: None, format: ReportOutputFormat::Pretty },
+ baseline_file: None,
+ manage_baseline: false,
+ skip_regex: Vec::new(),
+ skip_word: Vec::new(),
+ };
+
+ let record = reporter.build_finding_record(&report_match, &scan_args);
+ assert_eq!(record.finding.path, blob_path);
+ let git_file_path = record
+ .finding
+ .git_metadata
+ .as_ref()
+ .and_then(|git| git.get("file"))
+ .and_then(|file| file.get("path"))
+ .and_then(|path| path.as_str())
+ .unwrap();
+ assert_eq!(git_file_path, "path/in/history.txt");
+ }
+}
+
impl From for ReportMatch {
fn from(e: finding_data::FindingDataEntry) -> Self {
ReportMatch {
From 645bfa2e01a1a41673ee941d7079ad9bfe317d0e Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Wed, 24 Sep 2025 10:06:47 -0700
Subject: [PATCH 11/12] Populate the finding path from git blob metadata so
history-derived secrets display their file location instead of an empty path
---
buildwin.bat | 1 +
data/rules/openweather.yml | 37 ------------------------------------
data/rules/travisci.yml | 2 +-
src/git_url.rs | 9 ++-------
src/reporter.rs | 39 ++++++++++++++++++++++++++++++++++++--
src/update.rs | 2 +-
6 files changed, 42 insertions(+), 48 deletions(-)
delete mode 100644 data/rules/openweather.yml
diff --git a/buildwin.bat b/buildwin.bat
index 55ca71f..deed257 100644
--- a/buildwin.bat
+++ b/buildwin.bat
@@ -20,6 +20,7 @@ if "%VCINSTALLDIR%"=="" (
echo VCINSTALLDIR not set - attempting auto-detection…
for %%P in (
"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC"
+ "C:\Program Files\Microsoft Visual Studio\2022\Community\VC"
"C:\Program Files\Microsoft Visual Studio\2022\Professional\VC"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC"
"C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC"
diff --git a/data/rules/openweather.yml b/data/rules/openweather.yml
deleted file mode 100644
index 2153e64..0000000
--- a/data/rules/openweather.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-rules:
- - name: OpenWeather Map API Key
- id: kingfisher.openweather.1
- pattern: |
- (?xi)
- (?:pyowm|openweather|\bowm\b)
- (?:.|[\n\r]){0,64}?
- \b
- (
- (?:
- [a-z0-9]{32}
- )
- \b
- |APPID=
- (?:
- [a-z0-9]{32}
- )
- )
- \b
- min_entropy: 3.5
- examples:
- - pyowm = '3k144a5af729351d0fc58bdrj9a21mkr'
- - owm = '3k144a5af729351d0fc58bdrj9a21mkr'
- - openweatherapikey=cd2b1d12d01ae2deffecfebafcc3c31d
- - apikey=openweather:cd2b1d12d01ae2deffecfebafcc3c31d
- validation:
- type: Http
- content:
- request:
- method: GET
- response_matcher:
- - report_response: true
- - match_all_status: true
- status:
- - 200
- type: StatusMatch
- url: https://api.openweathermap.org/geo/1.0/reverse?lat=0&lon=0&limit=1&appid={{ TOKEN }}
\ No newline at end of file
diff --git a/data/rules/travisci.yml b/data/rules/travisci.yml
index 5a61c0a..73e75c4 100644
--- a/data/rules/travisci.yml
+++ b/data/rules/travisci.yml
@@ -32,7 +32,7 @@ rules:
- type: StatusMatch
status: [200]
- name: Travis CI Encrypted Variable
- id: kingfisher.travisci.1
+ id: kingfisher.travisci.2
pattern: |
(?xis)
\b
diff --git a/src/git_url.rs b/src/git_url.rs
index 7458bcc..67e6e90 100644
--- a/src/git_url.rs
+++ b/src/git_url.rs
@@ -64,8 +64,8 @@ impl TryFrom for GitUrl {
type Error = &'static str;
fn try_from(url: Url) -> Result {
- // if url.scheme() != "https"
- if url.host().is_none()
+ if (url.scheme() != "https" && url.scheme() != "http")
+ || url.host().is_none()
|| !url.username().is_empty()
|| url.password().is_some()
|| url.query().is_some()
@@ -104,11 +104,6 @@ mod test {
assert!(GitUrl::from_str("ssh://example.com/repo.git").is_err());
}
- #[test]
- fn bad_scheme_04() {
- assert!(GitUrl::from_str("http://example.com/repo.git").is_err());
- }
-
#[test]
fn bad_query_params() {
assert!(GitUrl::from_str("https://example.com/repo.git?admin=1").is_err());
diff --git a/src/reporter.rs b/src/reporter.rs
index eccdcf1..5e9d49b 100644
--- a/src/reporter.rs
+++ b/src/reporter.rs
@@ -428,10 +428,10 @@ impl DetailsReporter {
})
.next();
- let file_path = rm
+ let mut file_path = rm
.origin
.iter()
- .find_map(|origin| match origin {
+ .filter_map(|origin| match origin {
Origin::File(e) => {
if let Some(url) = self.repo_artifact_url(&e.path) {
Some(url)
@@ -452,6 +452,7 @@ impl DetailsReporter {
Origin::GitRepo(e) => e.first_commit.as_ref().map(|c| c.blob_path.clone()),
Origin::Extended(e) => e.path().map(|p| p.display().to_string()),
})
+ .find(|path| !path.trim().is_empty())
.unwrap_or_else(|| {
rm.origin
.iter()
@@ -459,6 +460,31 @@ impl DetailsReporter {
.unwrap_or_default()
});
+ // If the file path is still empty, and we have git blob metadata,
+ // try to reconstruct the path from the git object ID.
+ if file_path.is_empty() {
+ let blob_hex = rm.blob_metadata.id.hex();
+ if let Some(repo_origin) = rm.origin.iter().find_map(|origin| match origin {
+ Origin::GitRepo(e) => Some(e),
+ _ => None,
+ }) {
+ let (prefix, suffix) = blob_hex.split_at(2);
+ let repo_path = repo_origin.repo_path.as_ref();
+ let git_dir_objects = repo_path.join(".git").join("objects");
+ let objects_dir = if git_dir_objects.is_dir() {
+ git_dir_objects
+ } else {
+ repo_path.join("objects")
+ };
+ let fallback_path = objects_dir.join(prefix).join(suffix);
+ file_path = fallback_path.display().to_string();
+ }
+
+ if file_path.is_empty() {
+ file_path = format!("blob:{blob_hex}");
+ }
+ }
+
FindingReporterRecord {
rule: RuleMetadata {
name: rm.m.rule.name().to_string(),
@@ -632,10 +658,12 @@ mod tests {
cli::commands::scan::{ConfidenceLevel, ScanArgs},
cli::commands::{
bitbucket::{BitbucketAuthArgs, BitbucketRepoType},
+ gitea::GiteaRepoType,
github::{GitCloneMode, GitHistoryMode, GitHubRepoType},
gitlab::GitLabRepoType,
rules::RuleSpecifierArgs,
},
+ git_commit_metadata::CommitMetadata,
location::{Location, OffsetSpan, SourcePoint, SourceSpan},
matcher::{SerializableCapture, SerializableCaptures},
origin::OriginSet,
@@ -737,6 +765,12 @@ mod tests {
gitlab_api_url: Url::parse("https://gitlab.com/").unwrap(),
gitlab_repo_type: GitLabRepoType::All,
gitlab_include_subgroups: false,
+ gitea_user: Vec::new(),
+ gitea_organization: Vec::new(),
+ gitea_exclude: Vec::new(),
+ all_gitea_organizations: false,
+ gitea_api_url: Url::parse("https://gitea.com/api/v1/").unwrap(),
+ gitea_repo_type: GiteaRepoType::Source,
bitbucket_user: Vec::new(),
bitbucket_workspace: Vec::new(),
bitbucket_project: Vec::new(),
@@ -779,6 +813,7 @@ mod tests {
rule_stats: false,
no_dedup: false,
redact: false,
+ no_base64: false,
git_repo_timeout: 1_800,
output_args: OutputArgs { output: None, format: ReportOutputFormat::Pretty },
baseline_file: None,
diff --git a/src/update.rs b/src/update.rs
index 878182d..b765768 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -102,7 +102,7 @@ pub fn check_for_update(global_args: &GlobalArgs, base_url: Option<&str>) -> Opt
// ───────────── Case 1: running == latest ─────────────
if release.version == running_v {
let plain = format!("Kingfisher {running_v} is up to date");
- info!("{}", styled_heading(&styles, plain.as_str()));
+ info!("{}", plain.as_str());
return Some(plain);
}
From 0c022b4ed5a4a5a655dfc666fd3e6fd1f693d5aa Mon Sep 17 00:00:00 2001
From: Mick Grove
Date: Wed, 24 Sep 2025 10:43:51 -0700
Subject: [PATCH 12/12] Changes in response to code review
---
src/reporter.rs | 116 ++++++++++++++++++++++++++----------------------
1 file changed, 63 insertions(+), 53 deletions(-)
diff --git a/src/reporter.rs b/src/reporter.rs
index 5e9d49b..caa6aa8 100644
--- a/src/reporter.rs
+++ b/src/reporter.rs
@@ -428,62 +428,20 @@ impl DetailsReporter {
})
.next();
- let mut file_path = rm
+ let file_path = rm
.origin
.iter()
- .filter_map(|origin| match origin {
- Origin::File(e) => {
- if let Some(url) = self.repo_artifact_url(&e.path) {
- Some(url)
- } else if let Some(url) = self.jira_issue_url(&e.path, args) {
- Some(url)
- } else if let Some(url) = self.confluence_page_url(&e.path) {
- Some(url)
- } else if let Some(url) = self.slack_message_url(&e.path) {
- Some(url)
- } else if let Some(mapped) = self.s3_display_path(&e.path) {
- Some(mapped)
- } else if let Some(mapped) = self.docker_display_path(&e.path) {
- Some(mapped)
- } else {
- Some(e.path.display().to_string())
- }
- }
- Origin::GitRepo(e) => e.first_commit.as_ref().map(|c| c.blob_path.clone()),
- Origin::Extended(e) => e.path().map(|p| p.display().to_string()),
+ .find_map(|origin| self.origin_display_path(origin, args))
+ .or_else(|| {
+ rm.origin.iter().find_map(|origin| {
+ origin
+ .blob_path()
+ .map(|p| p.display().to_string())
+ .and_then(Self::non_empty_string)
+ })
})
- .find(|path| !path.trim().is_empty())
- .unwrap_or_else(|| {
- rm.origin
- .iter()
- .find_map(|origin| origin.blob_path().map(|p| p.display().to_string()))
- .unwrap_or_default()
- });
-
- // If the file path is still empty, and we have git blob metadata,
- // try to reconstruct the path from the git object ID.
- if file_path.is_empty() {
- let blob_hex = rm.blob_metadata.id.hex();
- if let Some(repo_origin) = rm.origin.iter().find_map(|origin| match origin {
- Origin::GitRepo(e) => Some(e),
- _ => None,
- }) {
- let (prefix, suffix) = blob_hex.split_at(2);
- let repo_path = repo_origin.repo_path.as_ref();
- let git_dir_objects = repo_path.join(".git").join("objects");
- let objects_dir = if git_dir_objects.is_dir() {
- git_dir_objects
- } else {
- repo_path.join("objects")
- };
- let fallback_path = objects_dir.join(prefix).join(suffix);
- file_path = fallback_path.display().to_string();
- }
-
- if file_path.is_empty() {
- file_path = format!("blob:{blob_hex}");
- }
- }
+ .or_else(|| self.git_object_fallback_path(rm))
+ .unwrap_or_else(|| format!("blob:{}", rm.blob_metadata.id.hex()));
FindingReporterRecord {
rule: RuleMetadata {
@@ -511,6 +469,58 @@ impl DetailsReporter {
}
}
+ fn origin_display_path(
+ &self,
+ origin: &Origin,
+ args: &cli::commands::scan::ScanArgs,
+ ) -> Option {
+ match origin {
+ Origin::File(e) => self
+ .repo_artifact_url(&e.path)
+ .and_then(Self::non_empty_string)
+ .or_else(|| self.jira_issue_url(&e.path, args).and_then(Self::non_empty_string))
+ .or_else(|| self.confluence_page_url(&e.path).and_then(Self::non_empty_string))
+ .or_else(|| self.slack_message_url(&e.path).and_then(Self::non_empty_string))
+ .or_else(|| self.s3_display_path(&e.path).and_then(Self::non_empty_string))
+ .or_else(|| self.docker_display_path(&e.path).and_then(Self::non_empty_string))
+ .or_else(|| Self::non_empty_string(e.path.display().to_string())),
+ Origin::GitRepo(e) => {
+ e.first_commit.as_ref().and_then(|c| Self::non_empty_string(c.blob_path.clone()))
+ }
+ Origin::Extended(e) => {
+ e.path().map(|p| p.display().to_string()).and_then(Self::non_empty_string)
+ }
+ }
+ }
+
+ fn git_object_fallback_path(&self, rm: &ReportMatch) -> Option {
+ let blob_hex = rm.blob_metadata.id.hex();
+ rm.origin.iter().find_map(|origin| {
+ if let Origin::GitRepo(repo_origin) = origin {
+ let (prefix, suffix) = blob_hex.split_at(2);
+ let repo_path = repo_origin.repo_path.as_ref();
+ let git_dir_objects = repo_path.join(".git").join("objects");
+ let objects_dir = if git_dir_objects.is_dir() {
+ git_dir_objects
+ } else {
+ repo_path.join("objects")
+ };
+ let fallback_path = objects_dir.join(prefix).join(suffix);
+ Self::non_empty_string(fallback_path.display().to_string())
+ } else {
+ None
+ }
+ })
+ }
+
+ fn non_empty_string(value: String) -> Option {
+ if value.trim().is_empty() {
+ None
+ } else {
+ Some(value)
+ }
+ }
+
pub fn build_finding_records(
&self,
args: &cli::commands::scan::ScanArgs,