forked from mirrors/kingfisher
- New rules: Telegram bot token, OpenWeatherMap, Apify
- New OpenAI detectors added (@joshlarsen) - Fixed bug that broke validation when using unnamed group captures
This commit is contained in:
parent
362d2c272e
commit
46d0ecce3b
29 changed files with 241 additions and 64 deletions
|
|
@ -2,6 +2,11 @@
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.31.0]
|
||||||
|
- New rules: Telegram bot token, OpenWeatherMap, Apify
|
||||||
|
- New OpenAI detectors added (@joshlarsen)
|
||||||
|
- Fixed bug that broke validation when using unnamed group captures
|
||||||
|
|
||||||
## [1.30.0]
|
## [1.30.0]
|
||||||
- Fixed validation caching for HTTP validators to include rendered headers so inactive secrets no longer appear active.
|
- Fixed validation caching for HTTP validators to include rendered headers so inactive secrets no longer appear active.
|
||||||
- Removed pre-commit installation hook, due to bugs
|
- Removed pre-commit installation hook, due to bugs
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ publish = false
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kingfisher"
|
name = "kingfisher"
|
||||||
version = "1.30.0"
|
version = "1.31.0"
|
||||||
description = "MongoDB's blazingly fast secret scanning and validation tool"
|
description = "MongoDB's blazingly fast secret scanning and validation tool"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
|
|
@ -104,7 +104,6 @@ http = "1.3.1"
|
||||||
liquid = "0.26.11"
|
liquid = "0.26.11"
|
||||||
liquid-core = "0.26.11"
|
liquid-core = "0.26.11"
|
||||||
flate2 = "1.1.2"
|
flate2 = "1.1.2"
|
||||||
brotli = "6.0.0"
|
|
||||||
thousands = "0.2.0"
|
thousands = "0.2.0"
|
||||||
base32 = "0.5.1"
|
base32 = "0.5.1"
|
||||||
crossbeam-skiplist = "0.1.3"
|
crossbeam-skiplist = "0.1.3"
|
||||||
|
|
@ -172,13 +171,10 @@ color-backtrace = "0.7.0"
|
||||||
gitlab = "0.1801.0"
|
gitlab = "0.1801.0"
|
||||||
mimalloc = {version = "0.1.47", features = ["override"]}
|
mimalloc = {version = "0.1.47", features = ["override"]}
|
||||||
thread_local = "1.1.9"
|
thread_local = "1.1.9"
|
||||||
crc32fast = "1.5.0"
|
|
||||||
bloomfilter = "3.0.1"
|
bloomfilter = "3.0.1"
|
||||||
uuid = "1.17.0"
|
uuid = "1.17.0"
|
||||||
urlencoding = "2.1.3"
|
|
||||||
rand = "0.9.1"
|
rand = "0.9.1"
|
||||||
percent-encoding = "2.3.1"
|
percent-encoding = "2.3.1"
|
||||||
trust-dns-resolver = { version = "0.23.2", default-features = false, features = ["tokio-runtime"] }
|
|
||||||
atty = "0.2.14"
|
atty = "0.2.14"
|
||||||
self_update = { version = "0.42.0", default-features = false, features = ["rustls", "archive-tar", "archive-zip", "compression-flate2"] }
|
self_update = { version = "0.42.0", default-features = false, features = ["rustls", "archive-tar", "archive-zip", "compression-flate2"] }
|
||||||
semver = "1.0.26"
|
semver = "1.0.26"
|
||||||
|
|
@ -189,8 +185,6 @@ jira_query = "1.6.0"
|
||||||
oci-client = { version = "0.15", default-features = false, features = ["rustls-tls"] }
|
oci-client = { version = "0.15", default-features = false, features = ["rustls-tls"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
sec1 = "0.7.3"
|
|
||||||
rand_core = "0.9.3"
|
|
||||||
ed25519-dalek = { version = "2.2", features = ["pkcs8"] }
|
ed25519-dalek = { version = "2.2", features = ["pkcs8"] }
|
||||||
|
|
||||||
[dependencies.tikv-jemallocator]
|
[dependencies.tikv-jemallocator]
|
||||||
|
|
|
||||||
1
Makefile
1
Makefile
|
|
@ -24,6 +24,7 @@ endif
|
||||||
ifeq ($(OS),darwin)
|
ifeq ($(OS),darwin)
|
||||||
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
export HOMEBREW_NO_INSTALL_CLEANUP=1
|
||||||
export HOMEBREW_NO_ENV_HINTS=1
|
export HOMEBREW_NO_ENV_HINTS=1
|
||||||
|
export HOMEBREW_NO_AUTO_UPDATE=1
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# detect host architecture and map to our target suffixes
|
# detect host architecture and map to our target suffixes
|
||||||
|
|
|
||||||
35
data/rules/apify.yml
Normal file
35
data/rules/apify.yml
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
rules:
|
||||||
|
- name: Apify API Token
|
||||||
|
id: kingfisher.apify.1
|
||||||
|
pattern: |
|
||||||
|
(?xi)
|
||||||
|
\b
|
||||||
|
(
|
||||||
|
apify_api_[A-Z0-9]{34,38}
|
||||||
|
)
|
||||||
|
\b
|
||||||
|
confidence: medium
|
||||||
|
min_entropy: 3.5
|
||||||
|
validation:
|
||||||
|
type: Http
|
||||||
|
content:
|
||||||
|
request:
|
||||||
|
method: GET
|
||||||
|
url: "https://api.apify.com/v2/users/me"
|
||||||
|
headers:
|
||||||
|
Authorization: "Bearer {{ TOKEN }}"
|
||||||
|
response_matcher:
|
||||||
|
- type: StatusMatch
|
||||||
|
status: [200]
|
||||||
|
- type: WordMatch
|
||||||
|
words:
|
||||||
|
- '"data"'
|
||||||
|
- '"username"'
|
||||||
|
match_all_words: true
|
||||||
|
references:
|
||||||
|
- https://docs.gitguardian.com/secrets-detection/secrets-detection-engine/detectors/specifics/apify_token
|
||||||
|
- https://docs.apify.com/api/v2#/reference/users/user-object/get-user-public-profile-or-me
|
||||||
|
- https://docs.apify.com/api/v2/users-me-get
|
||||||
|
examples:
|
||||||
|
- "apify_api_NcjXcxEz2XL1irjppyWSHvjghalQOd1LXOHv"
|
||||||
|
- "apify_api_9uyewBxQUF1EXWdKVc4lNaTSM461Ls4oQouz"
|
||||||
42
data/rules/openweathermap.yml
Normal file
42
data/rules/openweathermap.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
rules:
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# 1. OpenWeather Map API Key (detector unchanged, new validation)
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
- name: OpenWeather Map API Key
|
||||||
|
id: kingfisher.openweather.1
|
||||||
|
pattern: |
|
||||||
|
(?xi)
|
||||||
|
\b
|
||||||
|
(?:pyowm|openweather|owm\b)
|
||||||
|
(?:.|[\n\r]){0,64}?
|
||||||
|
\b
|
||||||
|
(
|
||||||
|
[a-z0-9]{32}
|
||||||
|
|
|
||||||
|
APPID=
|
||||||
|
[a-z0-9]{32}
|
||||||
|
)
|
||||||
|
\b
|
||||||
|
min_entropy: 3.5
|
||||||
|
confidence: medium
|
||||||
|
examples:
|
||||||
|
- pyowm = '3k144a5af729351d0fc58bdrj9a21mkr'
|
||||||
|
- owm = '3k144a5af729351d0fc58bdrj9a21mkr'
|
||||||
|
- openweatherapikey=cd2b1d12d01ae2deffecfebafcc3c31d
|
||||||
|
- apikey=openweather:cd2b1d12d01ae2deffecfebafcc3c31d
|
||||||
|
validation:
|
||||||
|
type: Http
|
||||||
|
content:
|
||||||
|
request:
|
||||||
|
method: GET
|
||||||
|
url: https://api.openweathermap.org/data/2.5/forecast?q=London&appid={{ TOKEN }}
|
||||||
|
response_matcher:
|
||||||
|
- report_response: true
|
||||||
|
- type: StatusMatch
|
||||||
|
status: [200]
|
||||||
|
words: ['"cod":"200"']
|
||||||
|
references:
|
||||||
|
- https://openweathermap.org/forecast5
|
||||||
|
- https://openweathermap.org/appid
|
||||||
|
- https://publicapi.dev/open-weather-map-api
|
||||||
|
|
||||||
|
|
@ -5,8 +5,6 @@ rules:
|
||||||
(?xi)
|
(?xi)
|
||||||
recaptcha
|
recaptcha
|
||||||
(?:.|[\n\r]){0,16}?
|
(?:.|[\n\r]){0,16}?
|
||||||
(?:SECRET|PRIVATE|ACCESS|KEY|TOKEN)
|
|
||||||
(?:.|[\n\r]){0,32}?
|
|
||||||
\b
|
\b
|
||||||
(
|
(
|
||||||
6l[c-f][a-z0-9_-].{36}
|
6l[c-f][a-z0-9_-].{36}
|
||||||
|
|
|
||||||
30
data/rules/telegram.yml
Normal file
30
data/rules/telegram.yml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
rules:
|
||||||
|
- name: Telegram Bot Token
|
||||||
|
id: kingfisher.telegram.1
|
||||||
|
pattern: |
|
||||||
|
(?xi)
|
||||||
|
\b
|
||||||
|
(
|
||||||
|
[0-9]{8,10}
|
||||||
|
:
|
||||||
|
[A-Z0-9_-]{35}
|
||||||
|
)
|
||||||
|
\b
|
||||||
|
confidence: medium
|
||||||
|
min_entropy: 3.5
|
||||||
|
validation:
|
||||||
|
type: Http
|
||||||
|
content:
|
||||||
|
request:
|
||||||
|
method: GET
|
||||||
|
url: "https://api.telegram.org/bot{{TOKEN}}/getMe"
|
||||||
|
response_matcher:
|
||||||
|
- type: StatusMatch
|
||||||
|
status: [200]
|
||||||
|
- type: WordMatch
|
||||||
|
words:
|
||||||
|
- '"ok":true'
|
||||||
|
examples:
|
||||||
|
- "110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawd"
|
||||||
|
- "508627689:AAEuLPKs-EhrjrYGnz60bnYNZqakf6HJxc0"
|
||||||
|
- "3628091811:BAG9RuJiqgOGIfFbOPBpAo6QhIJoD9mCdDs"
|
||||||
|
|
@ -108,7 +108,6 @@ Below is the complete list of Liquid filters available in Kingfisher, along with
|
||||||
| `b64enc` | – | Base64-encodes the input using the standard alphabet. | `{{ TOKEN \| b64enc }}` |
|
| `b64enc` | – | Base64-encodes the input using the standard alphabet. | `{{ TOKEN \| b64enc }}` |
|
||||||
| `b64url_enc` | – | URL-safe Base64 (no padding). Useful for JWT headers & payloads. | `{{ TOKEN \| b64url_enc }}` |
|
| `b64url_enc` | – | URL-safe Base64 (no padding). Useful for JWT headers & payloads. | `{{ TOKEN \| b64url_enc }}` |
|
||||||
| `b64dec` | – | Decodes a Base64 string. | `{{ "aGVsbG8=" \| b64dec }}` |
|
| `b64dec` | – | Decodes a Base64 string. | `{{ "aGVsbG8=" \| b64dec }}` |
|
||||||
| `es256_sign` | `key` (string) | Signs the input with an ECDSA P-256 private key and returns a Base64URL signature. | `{{ "data" \| es256_sign: PRIVKEY }}` |
|
|
||||||
| `sha256` | – | Computes the SHA-256 hex digest of the input. | `{{ TOKEN \| sha256 }}` |
|
| `sha256` | – | Computes the SHA-256 hex digest of the input. | `{{ TOKEN \| sha256 }}` |
|
||||||
| `hmac_sha1` | `key` (string) | Computes HMAC-SHA1 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha1: "secret-key" }}` |
|
| `hmac_sha1` | `key` (string) | Computes HMAC-SHA1 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha1: "secret-key" }}` |
|
||||||
| `hmac_sha256` | `key` (string) | Computes HMAC-SHA256 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha256: "secret-key" }}` |
|
| `hmac_sha256` | `key` (string) | Computes HMAC-SHA256 over the input, returns Base64-encoded result. | `{{ TOKEN \| hmac_sha256: "secret-key" }}` |
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,5 @@ tab_spaces = 4
|
||||||
use_small_heuristics = "Max"
|
use_small_heuristics = "Max"
|
||||||
newline_style = "Unix"
|
newline_style = "Unix"
|
||||||
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
group_imports = "StdExternalCrate"
|
|
||||||
reorder_imports = true
|
reorder_imports = true
|
||||||
|
|
||||||
normalize_doc_attributes = true
|
|
||||||
format_code_in_doc_comments = true
|
|
||||||
wrap_comments = true
|
|
||||||
comment_width = 100
|
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,11 @@ impl Git {
|
||||||
/// Create a new `Git` instance.
|
/// Create a new `Git` instance.
|
||||||
///
|
///
|
||||||
/// * `ignore_certs`: If `true`, disables SSL certificate verification for `git` operations.
|
/// * `ignore_certs`: If `true`, disables SSL certificate verification for `git` operations.
|
||||||
pub fn new(ignore_certs: bool) -> Self {
|
pub fn new(ignore_certs: bool) -> Self {
|
||||||
let mut credentials = Vec::new();
|
let mut credentials = Vec::new();
|
||||||
|
|
||||||
// If either GitHub or GitLab token is set, first clear existing credential.helpers
|
// If either GitHub or GitLab token is set, first clear existing credential.helpers
|
||||||
if std::env::var("KF_GITHUB_TOKEN").is_ok()
|
if std::env::var("KF_GITHUB_TOKEN").is_ok() || std::env::var("KF_GITLAB_TOKEN").is_ok() {
|
||||||
|| std::env::var("KF_GITLAB_TOKEN").is_ok()
|
|
||||||
{
|
|
||||||
credentials.push("-c".into());
|
credentials.push("-c".into());
|
||||||
credentials.push(r#"credential.helper="#.into());
|
credentials.push(r#"credential.helper="#.into());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ impl RepositoryIndex {
|
||||||
let mut num_trees = 0;
|
let mut num_trees = 0;
|
||||||
let mut num_blobs = 0;
|
let mut num_blobs = 0;
|
||||||
let mut num_commits = 0;
|
let mut num_commits = 0;
|
||||||
|
|
||||||
for oid in odb
|
for oid in odb
|
||||||
.iter()
|
.iter()
|
||||||
.context("Failed to iterate object database")?
|
.context("Failed to iterate object database")?
|
||||||
|
|
|
||||||
|
|
@ -49,4 +49,4 @@ pub async fn download_issues_to_dir(
|
||||||
paths.push(file);
|
paths.push(file);
|
||||||
}
|
}
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,6 @@ use liquid_core::{
|
||||||
FromFilterParameters, ParseFilter, Result, Runtime, Value, ValueView,
|
FromFilterParameters, ParseFilter, Result, Runtime, Value, ValueView,
|
||||||
};
|
};
|
||||||
|
|
||||||
use p256::ecdsa::{signature::Signer, SigningKey};
|
|
||||||
use p256::pkcs8::DecodePrivateKey;
|
|
||||||
use sec1::DecodeEcPrivateKey;
|
|
||||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||||
use rand::{distr::Alphanumeric, Rng};
|
use rand::{distr::Alphanumeric, Rng};
|
||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
|
|
@ -295,7 +292,6 @@ impl Filter for B64DecFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Authentication & Security
|
// Authentication & Security
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
|
||||||
// Slack query
|
// Slack query
|
||||||
slack_query: None,
|
slack_query: None,
|
||||||
slack_api_url: Url::parse("https://slack.com/api/").unwrap(),
|
slack_api_url: Url::parse("https://slack.com/api/").unwrap(),
|
||||||
|
|
||||||
// Docker image scanning
|
// Docker image scanning
|
||||||
docker_image: Vec::new(),
|
docker_image: Vec::new(),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,6 @@ impl DetailsReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// If the given file path corresponds to a Jira issue downloaded to disk,
|
/// If the given file path corresponds to a Jira issue downloaded to disk,
|
||||||
/// return the online Jira URL for that issue.
|
/// return the online Jira URL for that issue.
|
||||||
fn jira_issue_url(
|
fn jira_issue_url(
|
||||||
|
|
@ -123,7 +122,7 @@ impl DetailsReporter {
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
// drop any trailing slash so we don’t end up with “//browse/…”
|
// drop any trailing slash so we don’t end up with “//browse/…”
|
||||||
let jira_url = args.input_specifier_args.jira_url.as_ref()?.as_str().trim_end_matches('/');
|
let jira_url = args.input_specifier_args.jira_url.as_ref()?.as_str().trim_end_matches('/');
|
||||||
|
|
||||||
let ds = self.datastore.lock().ok()?;
|
let ds = self.datastore.lock().ok()?;
|
||||||
let root = ds.clone_root();
|
let root = ds.clone_root();
|
||||||
let jira_dir = root.join("jira_issues");
|
let jira_dir = root.join("jira_issues");
|
||||||
|
|
|
||||||
|
|
@ -441,7 +441,7 @@ mod tests {
|
||||||
// Slack options
|
// Slack options
|
||||||
slack_query: None,
|
slack_query: None,
|
||||||
slack_api_url: Url::parse("https://slack.com/api/").unwrap(),
|
slack_api_url: Url::parse("https://slack.com/api/").unwrap(),
|
||||||
|
|
||||||
docker_image: Vec::new(),
|
docker_image: Vec::new(),
|
||||||
// clone / history options
|
// clone / history options
|
||||||
git_clone: GitCloneMode::Bare,
|
git_clone: GitCloneMode::Bare,
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,6 @@ impl DetailsReporter {
|
||||||
let p = first_match.origin.first();
|
let p = first_match.origin.first();
|
||||||
match p {
|
match p {
|
||||||
Origin::File(e) => {
|
Origin::File(e) => {
|
||||||
|
|
||||||
let uri = if let Some(url) = self.jira_issue_url(&e.path, args) {
|
let uri = if let Some(url) = self.jira_issue_url(&e.path, args) {
|
||||||
url
|
url
|
||||||
} else if let Some(url) = self.slack_message_url(&e.path) {
|
} else if let Some(url) = self.slack_message_url(&e.path) {
|
||||||
|
|
@ -351,7 +350,7 @@ impl DetailsReporter {
|
||||||
.build()?,
|
.build()?,
|
||||||
)
|
)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let sarif_results: Vec<sarif::Result> = findings
|
let sarif_results: Vec<sarif::Result> = findings
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|f| self.make_sarif_result(f, no_dedup, args).ok())
|
.filter_map(|f| self.make_sarif_result(f, no_dedup, args).ok())
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ fn image_dir_name(reference: &str) -> String {
|
||||||
|
|
||||||
// add a truncated SHA-256 to guarantee uniqueness
|
// add a truncated SHA-256 to guarantee uniqueness
|
||||||
let hash = Sha256::digest(reference.as_bytes());
|
let hash = Sha256::digest(reference.as_bytes());
|
||||||
let short = &hex::encode(hash)[..8]; // 8-char prefix is plenty
|
let short = &hex::encode(hash)[..8]; // 8-char prefix is plenty
|
||||||
name.push('_');
|
name.push('_');
|
||||||
name.push_str(short);
|
name.push_str(short);
|
||||||
name
|
name
|
||||||
|
|
@ -258,7 +258,7 @@ pub async fn save_docker_images(
|
||||||
) -> Result<Vec<(PathBuf, String)>> {
|
) -> Result<Vec<(PathBuf, String)>> {
|
||||||
let docker = Docker::new();
|
let docker = Docker::new();
|
||||||
let mut dirs = Vec::new();
|
let mut dirs = Vec::new();
|
||||||
|
|
||||||
for image in images {
|
for image in images {
|
||||||
let dir_name = image_dir_name(image);
|
let dir_name = image_dir_name(image);
|
||||||
let out_dir = clone_root.join(format!("docker_{dir_name}"));
|
let out_dir = clone_root.join(format!("docker_{dir_name}"));
|
||||||
|
|
@ -280,4 +280,4 @@ mod tests {
|
||||||
fn docker_struct_new() {
|
fn docker_struct_new() {
|
||||||
let _ = Docker::new();
|
let _ = Docker::new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
//! Public façade for the scanner subsystem.
|
//! Public façade for the scanner subsystem.
|
||||||
|
pub(crate) use docker::save_docker_images;
|
||||||
pub(crate) use enumerate::enumerate_filesystem_inputs;
|
pub(crate) use enumerate::enumerate_filesystem_inputs;
|
||||||
pub(crate) use repos::{clone_or_update_git_repos, enumerate_github_repos};
|
pub(crate) use repos::{clone_or_update_git_repos, enumerate_github_repos};
|
||||||
pub use runner::{load_and_record_rules, run_async_scan, run_scan};
|
pub use runner::{load_and_record_rules, run_async_scan, run_scan};
|
||||||
pub(crate) use validation::run_secret_validation;
|
pub(crate) use validation::run_secret_validation;
|
||||||
pub(crate) use docker::save_docker_images;
|
|
||||||
|
|
||||||
mod docker;
|
mod docker;
|
||||||
mod enumerate;
|
mod enumerate;
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,6 @@ pub async fn enumerate_gitlab_repos(
|
||||||
Ok(repo_urls)
|
Ok(repo_urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn fetch_jira_issues(
|
pub async fn fetch_jira_issues(
|
||||||
args: &scan::ScanArgs,
|
args: &scan::ScanArgs,
|
||||||
global_args: &global::GlobalArgs,
|
global_args: &global::GlobalArgs,
|
||||||
|
|
@ -284,4 +283,4 @@ pub async fn fetch_slack_messages(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(vec![output_dir])
|
Ok(vec![output_dir])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ pub async fn run_async_scan(
|
||||||
// Fetch Jira issues if requested
|
// Fetch Jira issues if requested
|
||||||
let jira_dirs = fetch_jira_issues(args, global_args, &datastore).await?;
|
let jira_dirs = fetch_jira_issues(args, global_args, &datastore).await?;
|
||||||
input_roots.extend(jira_dirs);
|
input_roots.extend(jira_dirs);
|
||||||
|
|
||||||
// Fetch Slack messages if requested
|
// Fetch Slack messages if requested
|
||||||
let slack_dirs = fetch_slack_messages(args, global_args, &datastore).await?;
|
let slack_dirs = fetch_slack_messages(args, global_args, &datastore).await?;
|
||||||
input_roots.extend(slack_dirs);
|
input_roots.extend(slack_dirs);
|
||||||
|
|
|
||||||
|
|
@ -115,4 +115,4 @@ pub async fn download_messages_to_dir(
|
||||||
paths.push((file, msg.permalink));
|
paths.push((file, msg.permalink));
|
||||||
}
|
}
|
||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
fs,
|
fs,
|
||||||
hash::{Hash, Hasher},
|
hash::{Hash, Hasher},
|
||||||
collections::BTreeMap,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
@ -1043,5 +1043,4 @@ rules:
|
||||||
println!("Body: {:?}", owned_blob_match.validation_response_body);
|
println!("Body: {:?}", owned_blob_match.validation_response_body);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,16 +4,16 @@ use std::time::Duration;
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
use ed25519_dalek::SigningKey as Ed25519Key;
|
||||||
use p256::{
|
use p256::{
|
||||||
ecdsa::{signature::Signer as _, SigningKey},
|
ecdsa::{signature::Signer as _, SigningKey},
|
||||||
pkcs8::DecodePrivateKey,
|
pkcs8::DecodePrivateKey,
|
||||||
SecretKey,
|
SecretKey,
|
||||||
};
|
};
|
||||||
use ed25519_dalek::{SigningKey as Ed25519Key, Signer as _};
|
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
use rand::TryRngCore;
|
||||||
use reqwest::{Client, StatusCode, Url};
|
use reqwest::{Client, StatusCode, Url};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use rand::TryRngCore;
|
|
||||||
|
|
||||||
use crate::validation::{httpvalidation, Cache, CachedResponse, VALIDATION_CACHE_SECONDS};
|
use crate::validation::{httpvalidation, Cache, CachedResponse, VALIDATION_CACHE_SECONDS};
|
||||||
|
|
||||||
|
|
@ -68,7 +68,6 @@ pub async fn validate_cdp_api_key(
|
||||||
Ok((ok, msg))
|
Ok((ok, msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// fn build_jwt(
|
// fn build_jwt(
|
||||||
// method: &str,
|
// method: &str,
|
||||||
// host: &str,
|
// host: &str,
|
||||||
|
|
@ -85,7 +84,7 @@ pub async fn validate_cdp_api_key(
|
||||||
|
|
||||||
// let mut rng = OsRng;
|
// let mut rng = OsRng;
|
||||||
// let mut nonce = [0u8; 16];
|
// let mut nonce = [0u8; 16];
|
||||||
|
|
||||||
// let _ = rng.try_fill_bytes(&mut nonce);
|
// let _ = rng.try_fill_bytes(&mut nonce);
|
||||||
|
|
||||||
// let header = serde_json::json!({
|
// let header = serde_json::json!({
|
||||||
|
|
@ -125,12 +124,12 @@ fn build_jwt(
|
||||||
|
|
||||||
let mut rng = OsRng;
|
let mut rng = OsRng;
|
||||||
let mut nonce = [0u8; 16];
|
let mut nonce = [0u8; 16];
|
||||||
|
|
||||||
let _ = rng.try_fill_bytes(&mut nonce);
|
let _ = rng.try_fill_bytes(&mut nonce);
|
||||||
|
|
||||||
// Try ECDSA (PEM encoded EC key). Fallback to raw Ed25519 base64 key.
|
// Try ECDSA (PEM encoded EC key). Fallback to raw Ed25519 base64 key.
|
||||||
if let Ok(secret_key) = SecretKey::from_sec1_pem(&pem)
|
if let Ok(secret_key) =
|
||||||
.or_else(|_| SecretKey::from_pkcs8_pem(&pem))
|
SecretKey::from_sec1_pem(&pem).or_else(|_| SecretKey::from_pkcs8_pem(&pem))
|
||||||
{
|
{
|
||||||
let signing_key = SigningKey::from(secret_key);
|
let signing_key = SigningKey::from(secret_key);
|
||||||
let header = serde_json::json!({
|
let header = serde_json::json!({
|
||||||
|
|
@ -168,7 +167,8 @@ fn build_jwt(
|
||||||
}
|
}
|
||||||
64 => {
|
64 => {
|
||||||
let arr: [u8; 64] = key_bytes[..64].try_into().unwrap();
|
let arr: [u8; 64] = key_bytes[..64].try_into().unwrap();
|
||||||
Ed25519Key::from_keypair_bytes(&arr).map_err(|e| anyhow!("invalid Ed25519 key: {e}"))?
|
Ed25519Key::from_keypair_bytes(&arr)
|
||||||
|
.map_err(|e| anyhow!("invalid Ed25519 key: {e}"))?
|
||||||
}
|
}
|
||||||
_ => return Err(anyhow!("invalid Ed25519 key length")),
|
_ => return Err(anyhow!("invalid Ed25519 key length")),
|
||||||
};
|
};
|
||||||
|
|
@ -196,4 +196,4 @@ fn build_jwt(
|
||||||
let sig_b64 = URL_SAFE_NO_PAD.encode(sig.to_bytes());
|
let sig_b64 = URL_SAFE_NO_PAD.encode(sig.to_bytes());
|
||||||
return Ok(format!("{signing_input}.{sig_b64}"));
|
return Ok(format!("{signing_input}.{sig_b64}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,11 @@ pub async fn validate_jwt(token: &str) -> Result<(bool, String)> {
|
||||||
|
|
||||||
return Ok((
|
return Ok((
|
||||||
true,
|
true,
|
||||||
format!("JWT valid (alg: {:?}, iss: {issuer}, aud: {:?})", alg, extract_aud_strings(&claims)),
|
format!(
|
||||||
|
"JWT valid (alg: {:?}, iss: {issuer}, aud: {:?})",
|
||||||
|
alg,
|
||||||
|
extract_aud_strings(&claims)
|
||||||
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,19 @@ use crate::validation::SerializableCaptures;
|
||||||
/// * If it’s unnamed, fall back to `"TOKEN"`
|
/// * If it’s unnamed, fall back to `"TOKEN"`
|
||||||
/// * Skip the unnamed “whole-match” capture **only when** there are
|
/// * Skip the unnamed “whole-match” capture **only when** there are
|
||||||
/// additional captures to return.
|
/// additional captures to return.
|
||||||
pub fn process_captures(
|
pub fn process_captures(captures: &SerializableCaptures) -> Vec<(String, String, usize, usize)> {
|
||||||
captures: &SerializableCaptures,
|
|
||||||
) -> Vec<(String, String, usize, usize)> {
|
|
||||||
let multiple = captures.captures.len() > 1;
|
let multiple = captures.captures.len() > 1;
|
||||||
|
|
||||||
captures
|
captures
|
||||||
.captures
|
.captures
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|cap| !multiple || cap.name.is_some())
|
// Skip the whole-match capture (match_number == 0) only when there
|
||||||
|
// are additional captures. All other captures – named or unnamed –
|
||||||
|
// should be preserved.
|
||||||
|
.filter(|cap| !multiple || cap.match_number != 0)
|
||||||
.map(|cap| {
|
.map(|cap| {
|
||||||
let name = cap
|
let name =
|
||||||
.name
|
cap.name.as_ref().map(|n| n.to_uppercase()).unwrap_or_else(|| "TOKEN".to_string());
|
||||||
.as_ref()
|
|
||||||
.map(|n| n.to_uppercase())
|
|
||||||
.unwrap_or_else(|| "TOKEN".to_string());
|
|
||||||
(name, cap.value.clone().into_owned(), cap.start, cap.end)
|
(name, cap.value.clone().into_owned(), cap.start, cap.end)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -68,3 +66,90 @@ pub async fn check_url_resolvable(url: &Url) -> Result<(), Box<dyn std::error::E
|
||||||
let addr = format!("{}:{}", host, port);
|
let addr = format!("{}:{}", host, port);
|
||||||
lookup_host(addr).await?.next().ok_or_else(|| "Failed to resolve URL".into()).map(|_| ())
|
lookup_host(addr).await?.next().ok_or_else(|| "Failed to resolve URL".into()).map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// tests
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::matcher::{SerializableCapture, SerializableCaptures};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_unnamed_capture_is_returned() {
|
||||||
|
let captures = SerializableCaptures {
|
||||||
|
captures: vec![SerializableCapture {
|
||||||
|
name: None,
|
||||||
|
match_number: 0,
|
||||||
|
start: 1,
|
||||||
|
end: 4,
|
||||||
|
value: "abc".into(),
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let result = process_captures(&captures);
|
||||||
|
assert_eq!(result, vec![("TOKEN".to_string(), "abc".to_string(), 1usize, 4usize)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skips_whole_match_when_multiple() {
|
||||||
|
let captures = SerializableCaptures {
|
||||||
|
captures: vec![
|
||||||
|
SerializableCapture {
|
||||||
|
name: None,
|
||||||
|
match_number: 0,
|
||||||
|
start: 0,
|
||||||
|
end: 5,
|
||||||
|
value: "abcde".into(),
|
||||||
|
},
|
||||||
|
SerializableCapture {
|
||||||
|
name: Some("foo".to_string()),
|
||||||
|
match_number: -1,
|
||||||
|
start: 1,
|
||||||
|
end: 4,
|
||||||
|
value: "bcd".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let result = process_captures(&captures);
|
||||||
|
assert_eq!(result, vec![("FOO".to_string(), "bcd".to_string(), 1usize, 4usize)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn includes_unnamed_groups_but_skips_whole_match() {
|
||||||
|
let captures = SerializableCaptures {
|
||||||
|
captures: vec![
|
||||||
|
SerializableCapture {
|
||||||
|
name: None,
|
||||||
|
match_number: 0,
|
||||||
|
start: 0,
|
||||||
|
end: 6,
|
||||||
|
value: "aabbcc".into(),
|
||||||
|
},
|
||||||
|
SerializableCapture {
|
||||||
|
name: Some("foo".to_string()),
|
||||||
|
match_number: -1,
|
||||||
|
start: 0,
|
||||||
|
end: 2,
|
||||||
|
value: "aa".into(),
|
||||||
|
},
|
||||||
|
SerializableCapture {
|
||||||
|
name: None,
|
||||||
|
match_number: 1,
|
||||||
|
start: 4,
|
||||||
|
end: 6,
|
||||||
|
value: "cc".into(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let result = process_captures(&captures);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![
|
||||||
|
("FOO".to_string(), "aa".to_string(), 0usize, 2usize),
|
||||||
|
("TOKEN".to_string(), "cc".to_string(), 4usize, 6usize),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
||||||
all_gitlab_groups: false,
|
all_gitlab_groups: false,
|
||||||
gitlab_api_url: Url::parse("https://gitlab.com/")?,
|
gitlab_api_url: Url::parse("https://gitlab.com/")?,
|
||||||
gitlab_repo_type: GitLabRepoType::Owner,
|
gitlab_repo_type: GitLabRepoType::Owner,
|
||||||
|
|
||||||
jira_url: None,
|
jira_url: None,
|
||||||
jql: None,
|
jql: None,
|
||||||
max_results: 100,
|
max_results: 100,
|
||||||
|
|
|
||||||
|
|
@ -195,4 +195,4 @@ async fn test_scan_slack_messages() -> Result<()> {
|
||||||
};
|
};
|
||||||
assert!(findings > 0);
|
assert!(findings > 0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,4 +17,4 @@ fn smoke_scan_docker_image() -> anyhow::Result<()> {
|
||||||
.code(205)
|
.code(205)
|
||||||
.stdout(predicate::str::contains("Active Credential"));
|
.stdout(predicate::str::contains("Active Credential"));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue