forked from mirrors/kingfisher
preparing for v1.78.0
This commit is contained in:
parent
63f1d515ae
commit
5253204c2a
38 changed files with 1943 additions and 94 deletions
|
|
@ -7,6 +7,14 @@ All notable changes to this project will be documented in this file.
|
|||
- Fixed `validate_command` and `revoke_command` generation in scan output to include all required `--var` arguments for rules with `depends_on` sections (e.g., PubNub, Azure Storage). Commands now include dependent variables like `--var SUBSCRIPTIONTOKEN=<value>` or `--var AZURENAME=<value>`.
|
||||
- Updated Azure Storage validation to use `AZURENAME` variable (matching the `depends_on_rule` configuration) with `STORAGE_ACCOUNT` maintained as a backward-compatible alias.
|
||||
- Added internal `dependent_captures` field to match records to preserve variables from dependent rules through the validation pipeline for accurate command generation.
|
||||
- Added `--tls-mode <strict|lax|off>` global flag to control TLS certificate validation behavior during credential validation:
|
||||
- `strict` (default): Full WebPKI certificate validation with trusted CA chains, hostname verification, and expiration checks
|
||||
- `lax`: Accept self-signed or unknown CA certificates, useful for database connections (PostgreSQL, MySQL, MongoDB) and services using private CAs (e.g., Amazon RDS)
|
||||
- `off`: Disable all TLS validation (equivalent to legacy `--ignore-certs`)
|
||||
- Added rule-level `tls_mode` field allowing individual rules to opt into relaxed TLS validation when appropriate. Rules for PostgreSQL, MySQL, MongoDB, JDBC, and JWT now include `tls_mode: lax` by default.
|
||||
- The `--ignore-certs` flag remains supported as a deprecated alias for `--tls-mode=off` for backward compatibility.
|
||||
- Updated documentation to explain TLS validation modes and their security implications.
|
||||
- Added comprehensive test coverage for TLS mode functionality including unit tests, integration tests, and rule configuration verification.
|
||||
|
||||
## [v1.77.0]
|
||||
- Added `kingfisher revoke` subcommand for revoking leaked credentials directly with the provider.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ rules:
|
|||
confidence: medium
|
||||
validation:
|
||||
type: Jdbc
|
||||
tls_mode: lax
|
||||
examples:
|
||||
- jdbc:postgresql://db.example.com:5432/app?user=admin&password=s3cr3t
|
||||
- jdbc:mysql://admin:s3cr3t@prod.internal:3306/inventory
|
||||
|
|
|
|||
|
|
@ -25,4 +25,5 @@ rules:
|
|||
- https://datatracker.ietf.org/doc/html/rfc4648
|
||||
- https://developer.okta.com/blog/2018/06/20/what-happens-if-your-jwt-is-stolen
|
||||
validation:
|
||||
type: JWT
|
||||
type: JWT
|
||||
tls_mode: lax
|
||||
|
|
@ -93,6 +93,7 @@ rules:
|
|||
- "mongodb://mongoadmin:contoso@something.foo.mongodb.net/myFirstDatabase"
|
||||
validation:
|
||||
type: MongoDB
|
||||
tls_mode: lax
|
||||
- name: MongoDB Atlas Service Account Token
|
||||
id: kingfisher.mongodb.4
|
||||
pattern: |
|
||||
|
|
@ -105,4 +106,5 @@ rules:
|
|||
examples:
|
||||
- mdb_sa_sk_BdIX_jLzut2WTgglKzKvSgWMDDj5hEoTqdwOyLOL
|
||||
validation:
|
||||
type: MongoDB
|
||||
type: MongoDB
|
||||
tls_mode: lax
|
||||
|
|
@ -44,3 +44,4 @@ rules:
|
|||
- mysql://user:pass@example.com:4406/app_db?ssl-mode=REQUIRED
|
||||
validation:
|
||||
type: MySQL
|
||||
tls_mode: lax
|
||||
|
|
|
|||
|
|
@ -38,4 +38,5 @@ rules:
|
|||
- CONNECTION_URI="postgis://postgres:s2Tf2k@rLMy@google.com:5434/elephant"
|
||||
- CONNECTION_URI="postgis://postgres:s2Tf2k@rLMy@google.com:5434/elephant"
|
||||
validation:
|
||||
type: Postgres
|
||||
type: Postgres
|
||||
tls_mode: lax
|
||||
|
|
@ -19,7 +19,7 @@ pub use rule::{
|
|||
ChecksumActual, ChecksumRequirement, Confidence, DependsOnRule, HttpRequest, HttpValidation,
|
||||
MultipartConfig, MultipartPart, PatternRequirementContext, PatternRequirements,
|
||||
PatternValidationResult, ReportResponseData, ResponseMatcher, Revocation, Rule, RuleSyntax,
|
||||
Validation, RULE_COMMENTS_PATTERN,
|
||||
TlsMode, Validation, RULE_COMMENTS_PATTERN,
|
||||
};
|
||||
|
||||
// Re-export Rules collection
|
||||
|
|
|
|||
|
|
@ -37,6 +37,33 @@ fn default_true() -> bool {
|
|||
true
|
||||
}
|
||||
|
||||
/// TLS certificate validation mode for secret validation requests.
|
||||
///
|
||||
/// Controls how TLS certificates are validated when connecting to endpoints
|
||||
/// during credential validation (e.g., database connections, API calls).
|
||||
#[derive(
|
||||
Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TlsMode {
|
||||
/// Full WebPKI certificate validation: trusted CA chain, hostname match, not expired.
|
||||
/// This is the default and most secure mode.
|
||||
#[default]
|
||||
Strict,
|
||||
|
||||
/// Accept self-signed or unknown CA certificates, but still enforce:
|
||||
/// - Hostname must match certificate's CN/SAN
|
||||
/// - Certificate must not be expired
|
||||
/// - TLS 1.2 or higher required
|
||||
///
|
||||
/// Useful for database connections (PostgreSQL, MySQL, MongoDB) that often use
|
||||
/// self-signed certificates or private CAs (e.g., Amazon RDS).
|
||||
Lax,
|
||||
|
||||
/// Disable all TLS certificate validation. Use with extreme caution.
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Represents various types of validation that a rule can perform.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
#[serde(tag = "type", content = "content")]
|
||||
|
|
@ -534,6 +561,16 @@ pub struct RuleSyntax {
|
|||
/// Optional character type requirements for matched secrets.
|
||||
#[serde(default)]
|
||||
pub pattern_requirements: Option<PatternRequirements>,
|
||||
/// TLS validation mode for this rule's validation requests.
|
||||
///
|
||||
/// When set to `Lax`, the rule opts into relaxed TLS validation
|
||||
/// (accepting self-signed/unknown CA certs) when the user enables
|
||||
/// `--tls-mode=lax` on the command line.
|
||||
///
|
||||
/// This is useful for rules that validate against endpoints commonly
|
||||
/// using self-signed certificates, such as database connections.
|
||||
#[serde(default)]
|
||||
pub tls_mode: Option<TlsMode>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
|
@ -590,6 +627,7 @@ impl RuleSyntax {
|
|||
/// revocation: None,
|
||||
/// depends_on_rule: vec![],
|
||||
/// pattern_requirements: None,
|
||||
/// tls_mode: None,
|
||||
/// };
|
||||
/// assert_eq!(r.as_anchored_regex().unwrap().as_str(), r"hello\s*world$");
|
||||
/// ```
|
||||
|
|
@ -710,6 +748,15 @@ impl Rule {
|
|||
pub fn pattern_requirements(&self) -> Option<&PatternRequirements> {
|
||||
self.syntax.pattern_requirements.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the TLS validation mode for this rule, if specified.
|
||||
///
|
||||
/// When a rule returns `Some(TlsMode::Lax)`, it indicates the rule
|
||||
/// is eligible for relaxed TLS validation when the user enables
|
||||
/// `--tls-mode=lax` on the command line.
|
||||
pub fn tls_mode(&self) -> Option<TlsMode> {
|
||||
self.syntax.tls_mode
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -1006,4 +1053,104 @@ mod tests {
|
|||
assert!(matches!(reqs.validate(b"123", None, true), PatternValidationResult::Passed));
|
||||
assert!(matches!(reqs.validate(b"!@#", None, true), PatternValidationResult::Passed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_mode_default_is_strict() {
|
||||
assert_eq!(TlsMode::default(), TlsMode::Strict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_mode_serializes_to_lowercase() {
|
||||
assert_eq!(serde_yaml::to_string(&TlsMode::Strict).unwrap().trim(), "strict");
|
||||
assert_eq!(serde_yaml::to_string(&TlsMode::Lax).unwrap().trim(), "lax");
|
||||
assert_eq!(serde_yaml::to_string(&TlsMode::Off).unwrap().trim(), "off");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_mode_deserializes_from_lowercase() {
|
||||
let strict: TlsMode = serde_yaml::from_str("strict").unwrap();
|
||||
assert_eq!(strict, TlsMode::Strict);
|
||||
|
||||
let lax: TlsMode = serde_yaml::from_str("lax").unwrap();
|
||||
assert_eq!(lax, TlsMode::Lax);
|
||||
|
||||
let off: TlsMode = serde_yaml::from_str("off").unwrap();
|
||||
assert_eq!(off, TlsMode::Off);
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestRules {
|
||||
rules: Vec<RuleSyntax>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_syntax_parses_tls_mode_from_yaml() {
|
||||
let yaml = r#"
|
||||
rules:
|
||||
- name: Test Rule
|
||||
id: test.rule.1
|
||||
pattern: "test"
|
||||
tls_mode: lax
|
||||
"#;
|
||||
let parsed: TestRules = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(parsed.rules.len(), 1);
|
||||
assert_eq!(parsed.rules[0].tls_mode, Some(TlsMode::Lax));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_syntax_tls_mode_defaults_to_none_when_missing() {
|
||||
let yaml = r#"
|
||||
rules:
|
||||
- name: Test Rule
|
||||
id: test.rule.1
|
||||
pattern: "test"
|
||||
"#;
|
||||
let parsed: TestRules = serde_yaml::from_str(yaml).unwrap();
|
||||
assert_eq!(parsed.rules.len(), 1);
|
||||
assert_eq!(parsed.rules[0].tls_mode, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_tls_mode_method_returns_syntax_value() {
|
||||
let rule = Rule::new(RuleSyntax {
|
||||
name: "Test".to_string(),
|
||||
id: "test.1".to_string(),
|
||||
pattern: "test".to_string(),
|
||||
min_entropy: 0.0,
|
||||
confidence: Confidence::Low,
|
||||
visible: true,
|
||||
examples: vec![],
|
||||
negative_examples: vec![],
|
||||
references: vec![],
|
||||
validation: None,
|
||||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: Some(TlsMode::Lax),
|
||||
});
|
||||
|
||||
assert_eq!(rule.tls_mode(), Some(TlsMode::Lax));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_tls_mode_method_returns_none_when_not_set() {
|
||||
let rule = Rule::new(RuleSyntax {
|
||||
name: "Test".to_string(),
|
||||
id: "test.1".to_string(),
|
||||
pattern: "test".to_string(),
|
||||
min_entropy: 0.0,
|
||||
confidence: Confidence::Low,
|
||||
visible: true,
|
||||
examples: vec![],
|
||||
negative_examples: vec![],
|
||||
references: vec![],
|
||||
validation: None,
|
||||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
|
||||
assert_eq!(rule.tls_mode(), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -566,6 +566,7 @@ mod tests {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
})];
|
||||
|
||||
let rules_db = Arc::new(RulesDatabase::from_rules(rules).unwrap());
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ This guide covers all scan targets and usage patterns for Kingfisher.
|
|||
- [Jira](#jira)
|
||||
- [Confluence](#confluence)
|
||||
- [Slack](#slack)
|
||||
- [TLS Certificate Validation](#tls-certificate-validation)
|
||||
- [Environment Variables](#environment-variables)
|
||||
- [Exit Codes](#exit-codes)
|
||||
|
||||
|
|
@ -764,7 +765,7 @@ Bitbucket no longer supports App Tokens as of September 9, 2025: https://support
|
|||
|
||||
### Self-hosted Bitbucket Server
|
||||
|
||||
Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--ignore-certs` when connecting to HTTP or otherwise insecure instances.
|
||||
Use `--bitbucket-api-url` to point Kingfisher at your server's REST endpoint, for example `https://bitbucket.example.com/rest/api/1.0/`. Provide credentials with `KF_BITBUCKET_USERNAME` plus either `KF_BITBUCKET_TOKEN` or `KF_BITBUCKET_PASSWORD`, and pass `--tls-mode=off` (or the legacy `--ignore-certs`) when connecting to HTTP or otherwise insecure instances.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -869,6 +870,43 @@ KF_SLACK_TOKEN="xoxp-1234..." kingfisher scan slack "akia" \
|
|||
|
||||
---
|
||||
|
||||
## TLS Certificate Validation
|
||||
|
||||
Kingfisher validates TLS certificates when connecting to endpoints during secret validation (database connections, API calls, JWKS fetching, etc.). The `--tls-mode` flag controls this behavior:
|
||||
|
||||
| Mode | Description |
|
||||
| ---- | ----------- |
|
||||
| `strict` | **Default.** Full WebPKI certificate validation: trusted CA chain, hostname match, certificate not expired. |
|
||||
| `lax` | Accept self-signed or unknown CA certificates for rules that opt into it. Still enforces TLS 1.2+. Useful for database connections using self-signed certs or private CAs (e.g., Amazon RDS). |
|
||||
| `off` | Disable all certificate validation. Use with extreme caution. |
|
||||
|
||||
### When to use `--tls-mode=lax`
|
||||
|
||||
The `lax` mode is designed for environments where:
|
||||
|
||||
- **Database connections** use self-signed certificates (common for PostgreSQL, MySQL, MongoDB)
|
||||
- **Private CAs** are used (e.g., Amazon RDS uses an Amazon-issued CA that may not be in your system trust store)
|
||||
- **Internal services** have certificates not signed by public CAs
|
||||
|
||||
Rules must opt into lax TLS by declaring `tls_mode: lax` in their definition. When you pass `--tls-mode=lax`, only rules with this declaration will use relaxed certificate validation. SaaS API validators (GitHub, Slack, AWS, etc.) always use strict validation regardless of this flag.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Default: strict TLS everywhere
|
||||
kingfisher scan ./repo
|
||||
|
||||
# Lax TLS for database connection rules (Postgres, MySQL, MongoDB, JDBC, JWT)
|
||||
kingfisher scan --tls-mode=lax ./repo
|
||||
|
||||
# Disable all TLS validation (not recommended)
|
||||
kingfisher scan --tls-mode=off ./repo
|
||||
```
|
||||
|
||||
The legacy `--ignore-certs` flag is still supported as an alias for `--tls-mode=off`.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose |
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ mod tests {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@ impl CommandLineArgs {
|
|||
args.global_args.progress = Mode::Never;
|
||||
}
|
||||
|
||||
// Handle deprecated --ignore-certs flag as alias for --tls-mode=off
|
||||
if args.global_args.ignore_certs {
|
||||
args.global_args.tls_mode = TlsMode::Off;
|
||||
}
|
||||
|
||||
if let Some(suffix) = args.global_args.user_agent_suffix.as_mut() {
|
||||
let trimmed = suffix.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
|
@ -106,8 +111,16 @@ pub struct GlobalArgs {
|
|||
#[arg(global = true, long, short)]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Ignore TLS certificate validation
|
||||
#[arg(global = true, long)]
|
||||
/// TLS certificate validation mode for secret validation requests.
|
||||
///
|
||||
/// - strict: Full WebPKI validation (default)
|
||||
/// - lax: Accept self-signed/unknown CA, but enforce hostname + expiry
|
||||
/// - off: Disable all certificate validation
|
||||
#[arg(global = true, long, value_enum, default_value = "strict")]
|
||||
pub tls_mode: TlsMode,
|
||||
|
||||
/// Disable TLS certificate validation (deprecated: use --tls-mode=off)
|
||||
#[arg(global = true, long, hide = true)]
|
||||
pub ignore_certs: bool,
|
||||
|
||||
/// Update the Kingfisher binary to the latest release
|
||||
|
|
@ -135,6 +148,7 @@ impl Default for GlobalArgs {
|
|||
Self {
|
||||
verbose: 0,
|
||||
quiet: false,
|
||||
tls_mode: TlsMode::Strict,
|
||||
ignore_certs: false,
|
||||
self_update: false,
|
||||
no_update_check: false,
|
||||
|
|
@ -186,3 +200,65 @@ pub enum Mode {
|
|||
Never,
|
||||
Always,
|
||||
}
|
||||
|
||||
/// TLS certificate validation mode for secret validation requests.
|
||||
///
|
||||
/// Controls how TLS certificates are validated when connecting to endpoints
|
||||
/// during credential validation (e.g., database connections, API calls).
|
||||
#[derive(Copy, Clone, Debug, Display, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Default)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum TlsMode {
|
||||
/// Full WebPKI certificate validation: trusted CA chain, hostname match, not expired.
|
||||
/// This is the default and most secure mode.
|
||||
#[default]
|
||||
Strict,
|
||||
|
||||
/// Accept self-signed or unknown CA certificates, but still enforce:
|
||||
/// - Hostname must match certificate's CN/SAN
|
||||
/// - Certificate must not be expired
|
||||
/// - TLS 1.2 or higher required
|
||||
///
|
||||
/// Useful for database connections (PostgreSQL, MySQL, MongoDB) that often use
|
||||
/// self-signed certificates or private CAs (e.g., Amazon RDS).
|
||||
Lax,
|
||||
|
||||
/// Disable all TLS certificate validation. Use with extreme caution.
|
||||
/// Equivalent to the legacy `--ignore-certs` flag.
|
||||
Off,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tls_mode_default_is_strict() {
|
||||
assert_eq!(TlsMode::default(), TlsMode::Strict);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_mode_display_formats_correctly() {
|
||||
assert_eq!(TlsMode::Strict.to_string(), "strict");
|
||||
assert_eq!(TlsMode::Lax.to_string(), "lax");
|
||||
assert_eq!(TlsMode::Off.to_string(), "off");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_args_default_has_strict_tls() {
|
||||
let args = GlobalArgs::default();
|
||||
assert_eq!(args.tls_mode, TlsMode::Strict);
|
||||
assert!(!args.ignore_certs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_mode_ordering_is_correct() {
|
||||
// Strict < Lax < Off (more secure modes sort before less secure)
|
||||
assert!(TlsMode::Strict < TlsMode::Lax);
|
||||
assert!(TlsMode::Lax < TlsMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mode_default_is_auto() {
|
||||
assert_eq!(Mode::default(), Mode::Auto);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -344,9 +344,16 @@ pub async fn run_direct_validation(
|
|||
debug!("Rule selector '{}' matches {} rules, trying all", args.rule, num_matching_rules);
|
||||
}
|
||||
|
||||
// Determine if we should use lax TLS for non-HTTP validators
|
||||
// For direct validation (explicit user command), lax mode applies globally
|
||||
let use_lax_tls = matches!(
|
||||
global_args.tls_mode,
|
||||
crate::cli::global::TlsMode::Off | crate::cli::global::TlsMode::Lax
|
||||
);
|
||||
|
||||
// Build HTTP client
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(global_args.ignore_certs)
|
||||
.danger_accept_invalid_certs(use_lax_tls)
|
||||
.timeout(Duration::from_secs(args.timeout))
|
||||
.user_agent(GLOBAL_USER_AGENT.as_str())
|
||||
.gzip(true)
|
||||
|
|
@ -533,7 +540,7 @@ pub async fn run_direct_validation(
|
|||
|
||||
Validation::MongoDB => {
|
||||
// MongoDB expects a connection URI as the secret
|
||||
match validate_mongodb(&secret).await {
|
||||
match validate_mongodb(&secret, use_lax_tls).await {
|
||||
Ok((is_valid, message)) => DirectValidationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
|
|
@ -553,7 +560,7 @@ pub async fn run_direct_validation(
|
|||
|
||||
Validation::MySQL => {
|
||||
// MySQL expects a connection URL as the secret
|
||||
match validate_mysql(&secret).await {
|
||||
match validate_mysql(&secret, use_lax_tls).await {
|
||||
Ok((is_valid, metadata)) => DirectValidationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
|
|
@ -577,7 +584,7 @@ pub async fn run_direct_validation(
|
|||
|
||||
Validation::Postgres => {
|
||||
// Postgres expects a connection URL as the secret
|
||||
match validate_postgres(&secret).await {
|
||||
match validate_postgres(&secret, use_lax_tls).await {
|
||||
Ok((is_valid, metadata)) => DirectValidationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
|
|
@ -601,7 +608,7 @@ pub async fn run_direct_validation(
|
|||
|
||||
Validation::Jdbc => {
|
||||
// JDBC expects a JDBC connection string as the secret
|
||||
match validate_jdbc(&secret).await {
|
||||
match validate_jdbc(&secret, use_lax_tls).await {
|
||||
Ok(outcome) => DirectValidationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
|
|
@ -621,7 +628,7 @@ pub async fn run_direct_validation(
|
|||
|
||||
Validation::JWT => {
|
||||
// JWT expects a JWT token as the secret
|
||||
match validate_jwt(&secret).await {
|
||||
match validate_jwt(&secret, use_lax_tls).await {
|
||||
Ok((is_valid, message)) => DirectValidationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
|
|
|
|||
|
|
@ -1229,6 +1229,7 @@ mod test {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule]).unwrap();
|
||||
|
|
@ -1303,6 +1304,7 @@ mod test {
|
|||
}),
|
||||
],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
})];
|
||||
let rules_db = RulesDatabase::from_rules(rules)?;
|
||||
let input = "some test data for vectorscan";
|
||||
|
|
@ -1354,6 +1356,7 @@ mod test {
|
|||
ignore_if_contains: Some(vec!["TEST".to_string()]),
|
||||
checksum: None,
|
||||
}),
|
||||
tls_mode: None,
|
||||
})];
|
||||
|
||||
let rules_db = RulesDatabase::from_rules(rules)?;
|
||||
|
|
@ -1418,6 +1421,7 @@ mod test {
|
|||
ignore_if_contains: Some(vec!["TEST".to_string()]),
|
||||
checksum: None,
|
||||
}),
|
||||
tls_mode: None,
|
||||
})];
|
||||
|
||||
let rules_db = RulesDatabase::from_rules(rules)?;
|
||||
|
|
@ -1534,6 +1538,7 @@ mod test {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
|
|
@ -1574,6 +1579,7 @@ mod test {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
let seen = BlobIdMap::new();
|
||||
|
|
@ -1608,6 +1614,7 @@ mod test {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
let seen = BlobIdMap::new();
|
||||
|
|
@ -1650,6 +1657,7 @@ line2
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
});
|
||||
let rules_db = RulesDatabase::from_rules(vec![rule])?;
|
||||
|
||||
|
|
|
|||
|
|
@ -1263,6 +1263,7 @@ mod tests {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
}));
|
||||
|
||||
let blob_id = BlobId::new(b"blob-data");
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@ mod tests {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
};
|
||||
let rule = Arc::new(Rule::new(syntax));
|
||||
Match {
|
||||
|
|
|
|||
|
|
@ -277,10 +277,7 @@ pub async fn run_async_scan(
|
|||
info!("Starting secret validation phase...");
|
||||
Some(Arc::new((
|
||||
register_all(liquid::ParserBuilder::with_stdlib()).build()?,
|
||||
reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(global_args.ignore_certs)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.build()?,
|
||||
crate::validation::ValidationClients::new(global_args.tls_mode)?,
|
||||
Arc::new(SkipMap::new()),
|
||||
)))
|
||||
} else {
|
||||
|
|
@ -350,11 +347,11 @@ pub async fn run_async_scan(
|
|||
}
|
||||
|
||||
if let Some(validation) = &validation_deps {
|
||||
let (parser, client, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
let (parser, clients, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
run_secret_validation(
|
||||
Arc::clone(&datastore),
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
cache,
|
||||
args.num_jobs,
|
||||
None,
|
||||
|
|
@ -433,13 +430,13 @@ pub async fn run_async_scan(
|
|||
}
|
||||
|
||||
if let Some(validation) = &validation_deps {
|
||||
let (parser, client, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
let (parser, clients, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
let initial_match_count = { datastore.lock().unwrap().get_matches().len() };
|
||||
if initial_match_count > 0 {
|
||||
run_secret_validation(
|
||||
Arc::clone(&datastore),
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
cache,
|
||||
args.num_jobs,
|
||||
Some(0..initial_match_count),
|
||||
|
|
@ -515,7 +512,7 @@ pub async fn run_async_scan(
|
|||
}
|
||||
|
||||
if let Some(validation) = validation_deps.clone() {
|
||||
let (parser, client, cache) =
|
||||
let (parser, clients, cache) =
|
||||
(&validation.0, &validation.1, &validation.2);
|
||||
let match_count =
|
||||
{ repo_datastore.lock().unwrap().get_matches().len() };
|
||||
|
|
@ -523,7 +520,7 @@ pub async fn run_async_scan(
|
|||
rt_handle.block_on(run_secret_validation(
|
||||
Arc::clone(&repo_datastore),
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
cache,
|
||||
args.num_jobs,
|
||||
Some(0..match_count),
|
||||
|
|
@ -596,11 +593,11 @@ pub async fn run_async_scan(
|
|||
}
|
||||
|
||||
if let Some(validation) = &validation_deps {
|
||||
let (parser, client, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
let (parser, clients, cache) = (&validation.0, &validation.1, &validation.2);
|
||||
run_secret_validation(
|
||||
Arc::clone(&datastore),
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
cache,
|
||||
args.num_jobs,
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use dashmap::DashMap;
|
|||
use futures::{stream, StreamExt};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use liquid::Parser;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use reqwest::StatusCode;
|
||||
use rustc_hash::FxHashMap;
|
||||
use tokio::{sync::Notify, time::timeout};
|
||||
use tracing::trace;
|
||||
|
|
@ -108,7 +108,7 @@ impl AccessMapCollector {
|
|||
pub async fn run_secret_validation(
|
||||
datastore: Arc<Mutex<FindingsStore>>,
|
||||
parser: &Parser,
|
||||
client: &Client,
|
||||
clients: &crate::validation::ValidationClients,
|
||||
cache: &Arc<SkipMap<String, CachedResponse>>,
|
||||
num_jobs: usize,
|
||||
range: Option<std::ops::Range<usize>>,
|
||||
|
|
@ -205,7 +205,7 @@ pub async fn run_secret_validation(
|
|||
.for_each_concurrent(concurrency, |rep_arc| {
|
||||
// clones into task
|
||||
let parser = parser.clone();
|
||||
let client = client.clone();
|
||||
let clients = clients.clone();
|
||||
let cache_glob = cache.clone();
|
||||
let val_res = &validation_results;
|
||||
let success = success_count.clone();
|
||||
|
|
@ -241,7 +241,7 @@ pub async fn run_secret_validation(
|
|||
validate_single(
|
||||
&mut om,
|
||||
&parser,
|
||||
&client,
|
||||
&clients,
|
||||
&FxHashMap::default(),
|
||||
&FxHashMap::default(),
|
||||
&Arc::new(DashMap::new()),
|
||||
|
|
@ -318,7 +318,7 @@ pub async fn run_secret_validation(
|
|||
.map(|blob_id| {
|
||||
let matches_for_blob = dependent_blobs.get(blob_id).unwrap().clone();
|
||||
let parser = parser.clone();
|
||||
let client = client.clone();
|
||||
let clients = clients.clone();
|
||||
let val_cache = val_cache.clone();
|
||||
let in_flight = in_flight.clone();
|
||||
let success = success_count.clone();
|
||||
|
|
@ -352,7 +352,7 @@ pub async fn run_secret_validation(
|
|||
let validated: Vec<_> =
|
||||
stream::iter(reps.into_iter().map(|(mut rep, mut dups)| {
|
||||
let parser = parser.clone();
|
||||
let client = client.clone();
|
||||
let clients = clients.clone();
|
||||
let dep_vars = dep_vars.clone();
|
||||
let miss_deps = missing_deps.clone();
|
||||
let val_cache = val_cache.clone();
|
||||
|
|
@ -365,7 +365,7 @@ pub async fn run_secret_validation(
|
|||
validate_single(
|
||||
&mut rep,
|
||||
&parser,
|
||||
&client,
|
||||
&clients,
|
||||
&dep_vars,
|
||||
&miss_deps,
|
||||
&val_cache,
|
||||
|
|
@ -452,7 +452,7 @@ pub async fn run_secret_validation(
|
|||
async fn validate_single(
|
||||
om: &mut OwnedBlobMatch,
|
||||
parser: &Parser,
|
||||
client: &Client,
|
||||
clients: &crate::validation::ValidationClients,
|
||||
dep_vars: &FxHashMap<String, Vec<(String, OffsetSpan)>>,
|
||||
missing_deps: &FxHashMap<String, Vec<String>>,
|
||||
cache: &DashMap<String, CachedResponse>,
|
||||
|
|
@ -517,7 +517,7 @@ async fn validate_single(
|
|||
validate_single_match(
|
||||
om,
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
dep_vars,
|
||||
missing_deps,
|
||||
cache2,
|
||||
|
|
|
|||
|
|
@ -19,12 +19,16 @@ use tokio::{sync::Notify, time};
|
|||
use tracing::{debug, trace};
|
||||
|
||||
use crate::{
|
||||
cli::global::TlsMode,
|
||||
location::OffsetSpan,
|
||||
matcher::{OwnedBlobMatch, SerializableCaptures},
|
||||
rules::rule::Validation,
|
||||
validation_body::{self, ValidationResponseBody},
|
||||
};
|
||||
|
||||
// Re-export TlsMode from kingfisher_rules for use in client_for_rule
|
||||
pub use kingfisher_rules::TlsMode as RuleTlsMode;
|
||||
|
||||
pub mod aws;
|
||||
pub mod azure;
|
||||
pub mod coinbase;
|
||||
|
|
@ -88,6 +92,71 @@ pub fn set_user_agent_suffix<S: Into<String>>(suffix: Option<S>) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Holds HTTP clients for different TLS validation modes.
|
||||
///
|
||||
/// This struct is created once at scan startup and passed through the validation chain.
|
||||
/// The appropriate client is selected based on the global TLS mode and each rule's
|
||||
/// declared `tls_mode` setting.
|
||||
#[derive(Clone)]
|
||||
pub struct ValidationClients {
|
||||
/// Client with full TLS certificate validation (WebPKI chain, hostname, expiry).
|
||||
strict: Client,
|
||||
/// Client that accepts self-signed or invalid certificates.
|
||||
/// Used when `--tls-mode=lax` AND the rule opts into lax validation,
|
||||
/// or when `--tls-mode=off`.
|
||||
lax: Client,
|
||||
/// The global TLS mode from CLI arguments.
|
||||
pub global_mode: TlsMode,
|
||||
}
|
||||
|
||||
impl ValidationClients {
|
||||
/// Create validation clients based on the global TLS mode.
|
||||
pub fn new(global_mode: TlsMode) -> anyhow::Result<Self> {
|
||||
let timeout = std::time::Duration::from_secs(30);
|
||||
|
||||
let strict =
|
||||
Client::builder().danger_accept_invalid_certs(false).timeout(timeout).build()?;
|
||||
|
||||
let lax = Client::builder().danger_accept_invalid_certs(true).timeout(timeout).build()?;
|
||||
|
||||
Ok(Self { strict, lax, global_mode })
|
||||
}
|
||||
|
||||
/// Get the appropriate client for a given rule's TLS mode.
|
||||
///
|
||||
/// The effective TLS mode depends on both the global setting and the rule's preference:
|
||||
/// - If global mode is `Off`, always use the lax client (no validation).
|
||||
/// - If global mode is `Lax` and the rule declares `tls_mode: lax`, use lax client.
|
||||
/// - Otherwise, use the strict client.
|
||||
pub fn client_for_rule(&self, rule_tls_mode: Option<kingfisher_rules::TlsMode>) -> &Client {
|
||||
match self.global_mode {
|
||||
TlsMode::Off => &self.lax,
|
||||
TlsMode::Lax => {
|
||||
// Convert rule's TlsMode to CLI TlsMode for comparison
|
||||
let rule_wants_lax = matches!(rule_tls_mode, Some(kingfisher_rules::TlsMode::Lax));
|
||||
if rule_wants_lax {
|
||||
&self.lax
|
||||
} else {
|
||||
&self.strict
|
||||
}
|
||||
}
|
||||
TlsMode::Strict => &self.strict,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if lax TLS should be used for a rule.
|
||||
///
|
||||
/// This is useful for non-HTTP validators (Postgres, MySQL, etc.) that need to
|
||||
/// configure their own TLS settings.
|
||||
pub fn should_use_lax(&self, rule_tls_mode: Option<kingfisher_rules::TlsMode>) -> bool {
|
||||
match self.global_mode {
|
||||
TlsMode::Off => true,
|
||||
TlsMode::Lax => matches!(rule_tls_mode, Some(kingfisher_rules::TlsMode::Lax)),
|
||||
TlsMode::Strict => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use SkipMap-based cache instead of a mutex-wrapped FxHashMap.
|
||||
type Cache = Arc<SkipMap<String, CachedResponse>>;
|
||||
|
||||
|
|
@ -276,7 +345,7 @@ async fn render_template(
|
|||
pub async fn validate_single_match(
|
||||
m: &mut OwnedBlobMatch,
|
||||
parser: &liquid::Parser,
|
||||
client: &Client,
|
||||
clients: &ValidationClients,
|
||||
dependent_variables: &FxHashMap<String, Vec<(String, OffsetSpan)>>,
|
||||
missing_dependencies: &FxHashMap<String, Vec<String>>,
|
||||
cache: &Cache,
|
||||
|
|
@ -287,7 +356,7 @@ pub async fn validate_single_match(
|
|||
timed_validate_single_match(
|
||||
m,
|
||||
parser,
|
||||
client,
|
||||
clients,
|
||||
dependent_variables,
|
||||
missing_dependencies,
|
||||
cache,
|
||||
|
|
@ -314,13 +383,17 @@ pub async fn validate_single_match(
|
|||
async fn timed_validate_single_match<'a>(
|
||||
m: &mut OwnedBlobMatch,
|
||||
parser: &liquid::Parser,
|
||||
client: &Client,
|
||||
clients: &ValidationClients,
|
||||
dependent_variables: &FxHashMap<String, Vec<(String, OffsetSpan)>>,
|
||||
missing_dependencies: &FxHashMap<String, Vec<String>>,
|
||||
cache: &Cache,
|
||||
validation_timeout: Duration,
|
||||
validation_retries: u32,
|
||||
) {
|
||||
// Select the appropriate HTTP client based on rule's TLS mode preference
|
||||
let rule_tls_mode = m.rule.tls_mode();
|
||||
let client = clients.client_for_rule(rule_tls_mode);
|
||||
let use_lax_tls = clients.should_use_lax(rule_tls_mode);
|
||||
// ──────────────────────────────────────────────────────────
|
||||
// 1. process-wide fingerprint de-dup
|
||||
// ──────────────────────────────────────────────────────────
|
||||
|
|
@ -695,7 +768,7 @@ async fn timed_validate_single_match<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
match mongodb::validate_mongodb(&uri).await {
|
||||
match mongodb::validate_mongodb(&uri, use_lax_tls).await {
|
||||
Ok((ok, msg)) => {
|
||||
m.validation_success = ok;
|
||||
m.validation_response_body = validation_body::from_string(msg);
|
||||
|
|
@ -740,7 +813,7 @@ async fn timed_validate_single_match<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
match mysql::validate_mysql(&mysql_url).await {
|
||||
match mysql::validate_mysql(&mysql_url, use_lax_tls).await {
|
||||
Ok((ok, meta)) => {
|
||||
m.validation_success = ok;
|
||||
m.validation_response_body = validation_body::from_string(if ok {
|
||||
|
|
@ -862,7 +935,7 @@ async fn timed_validate_single_match<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
match jdbc::validate_jdbc(&jdbc_conn).await {
|
||||
match jdbc::validate_jdbc(&jdbc_conn, use_lax_tls).await {
|
||||
Ok(outcome) => {
|
||||
m.validation_success = outcome.valid;
|
||||
m.validation_response_body = validation_body::from_string(outcome.message);
|
||||
|
|
@ -916,7 +989,7 @@ async fn timed_validate_single_match<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
match postgres::validate_postgres(&pg_url).await {
|
||||
match postgres::validate_postgres(&pg_url, use_lax_tls).await {
|
||||
Ok((ok, meta)) => {
|
||||
m.validation_success = ok;
|
||||
m.validation_response_body = validation_body::from_string(if ok {
|
||||
|
|
@ -961,7 +1034,7 @@ async fn timed_validate_single_match<'a>(
|
|||
return;
|
||||
}
|
||||
|
||||
match jwt::validate_jwt(&token).await {
|
||||
match jwt::validate_jwt(&token, use_lax_tls).await {
|
||||
Ok((ok, msg)) => {
|
||||
m.validation_success = ok;
|
||||
m.validation_response_body = validation_body::from_string(msg);
|
||||
|
|
@ -1261,6 +1334,109 @@ mod tests {
|
|||
assert!(body.is_char_boundary(body.len()));
|
||||
assert!(body.ends_with('a'));
|
||||
}
|
||||
|
||||
mod tls_mode_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn validation_clients_new_creates_both_clients() {
|
||||
let clients = ValidationClients::new(TlsMode::Strict).unwrap();
|
||||
assert_eq!(clients.global_mode, TlsMode::Strict);
|
||||
|
||||
let clients_lax = ValidationClients::new(TlsMode::Lax).unwrap();
|
||||
assert_eq!(clients_lax.global_mode, TlsMode::Lax);
|
||||
|
||||
let clients_off = ValidationClients::new(TlsMode::Off).unwrap();
|
||||
assert_eq!(clients_off.global_mode, TlsMode::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_for_rule_strict_mode_always_returns_strict_client() {
|
||||
let clients = ValidationClients::new(TlsMode::Strict).unwrap();
|
||||
|
||||
// With no rule TLS mode
|
||||
let client1 = clients.client_for_rule(None);
|
||||
// With rule wanting lax
|
||||
let client2 = clients.client_for_rule(Some(kingfisher_rules::TlsMode::Lax));
|
||||
// With rule wanting strict
|
||||
let client3 = clients.client_for_rule(Some(kingfisher_rules::TlsMode::Strict));
|
||||
|
||||
// In strict mode, all should return the same strict client
|
||||
assert!(std::ptr::eq(client1, client2));
|
||||
assert!(std::ptr::eq(client2, client3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_for_rule_off_mode_always_returns_lax_client() {
|
||||
let clients = ValidationClients::new(TlsMode::Off).unwrap();
|
||||
|
||||
// With no rule TLS mode
|
||||
let client1 = clients.client_for_rule(None);
|
||||
// With rule wanting lax
|
||||
let client2 = clients.client_for_rule(Some(kingfisher_rules::TlsMode::Lax));
|
||||
// With rule wanting strict
|
||||
let client3 = clients.client_for_rule(Some(kingfisher_rules::TlsMode::Strict));
|
||||
|
||||
// In off mode, all should return the same lax client
|
||||
assert!(std::ptr::eq(client1, client2));
|
||||
assert!(std::ptr::eq(client2, client3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_for_rule_lax_mode_respects_rule_preference() {
|
||||
let clients = ValidationClients::new(TlsMode::Lax).unwrap();
|
||||
|
||||
// Get references to understand which is which
|
||||
let strict_client = clients.client_for_rule(None);
|
||||
let lax_client = clients.client_for_rule(Some(kingfisher_rules::TlsMode::Lax));
|
||||
|
||||
// When rule doesn't specify, should get strict
|
||||
assert!(std::ptr::eq(clients.client_for_rule(None), strict_client));
|
||||
|
||||
// When rule wants strict, should get strict
|
||||
assert!(std::ptr::eq(
|
||||
clients.client_for_rule(Some(kingfisher_rules::TlsMode::Strict)),
|
||||
strict_client
|
||||
));
|
||||
|
||||
// When rule wants lax, should get lax
|
||||
assert!(std::ptr::eq(
|
||||
clients.client_for_rule(Some(kingfisher_rules::TlsMode::Lax)),
|
||||
lax_client
|
||||
));
|
||||
|
||||
// Strict and lax clients should be different
|
||||
assert!(!std::ptr::eq(strict_client, lax_client));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_lax_off_mode_always_returns_true() {
|
||||
let clients = ValidationClients::new(TlsMode::Off).unwrap();
|
||||
|
||||
assert!(clients.should_use_lax(None));
|
||||
assert!(clients.should_use_lax(Some(kingfisher_rules::TlsMode::Strict)));
|
||||
assert!(clients.should_use_lax(Some(kingfisher_rules::TlsMode::Lax)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_lax_strict_mode_always_returns_false() {
|
||||
let clients = ValidationClients::new(TlsMode::Strict).unwrap();
|
||||
|
||||
assert!(!clients.should_use_lax(None));
|
||||
assert!(!clients.should_use_lax(Some(kingfisher_rules::TlsMode::Strict)));
|
||||
assert!(!clients.should_use_lax(Some(kingfisher_rules::TlsMode::Lax)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_use_lax_lax_mode_respects_rule_preference() {
|
||||
let clients = ValidationClients::new(TlsMode::Lax).unwrap();
|
||||
|
||||
// Only true when rule explicitly opts in
|
||||
assert!(!clients.should_use_lax(None));
|
||||
assert!(!clients.should_use_lax(Some(kingfisher_rules::TlsMode::Strict)));
|
||||
assert!(clients.should_use_lax(Some(kingfisher_rules::TlsMode::Lax)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,11 @@ pub fn generate_jdbc_cache_key(raw: &str) -> String {
|
|||
}
|
||||
|
||||
/// Validate a JDBC connection string by dispatching to the supported backend validators.
|
||||
pub async fn validate_jdbc(jdbc_conn: &str) -> Result<JdbcValidationOutcome> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `jdbc_conn` - The JDBC connection string to validate
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates
|
||||
pub async fn validate_jdbc(jdbc_conn: &str, lax_tls: bool) -> Result<JdbcValidationOutcome> {
|
||||
let trimmed = jdbc_conn.trim();
|
||||
if !trimmed.to_ascii_lowercase().starts_with("jdbc:") {
|
||||
return Err(anyhow!("JDBC connection string must start with `jdbc:`"));
|
||||
|
|
@ -33,9 +37,9 @@ pub async fn validate_jdbc(jdbc_conn: &str) -> Result<JdbcValidationOutcome> {
|
|||
let subprotocol_lower = subprotocol.to_ascii_lowercase();
|
||||
|
||||
match subprotocol_lower.as_str() {
|
||||
"postgres" | "postgresql" | "postgis" => {
|
||||
validate_postgres_jdbc(subname).await.context("Postgres JDBC validation failed")
|
||||
}
|
||||
"postgres" | "postgresql" | "postgis" => validate_postgres_jdbc(subname, lax_tls)
|
||||
.await
|
||||
.context("Postgres JDBC validation failed"),
|
||||
other => {
|
||||
debug!("Unsupported JDBC subprotocol encountered: {}", other);
|
||||
Ok(JdbcValidationOutcome {
|
||||
|
|
@ -50,9 +54,9 @@ pub async fn validate_jdbc(jdbc_conn: &str) -> Result<JdbcValidationOutcome> {
|
|||
}
|
||||
}
|
||||
|
||||
async fn validate_postgres_jdbc(subname: &str) -> Result<JdbcValidationOutcome> {
|
||||
async fn validate_postgres_jdbc(subname: &str, lax_tls: bool) -> Result<JdbcValidationOutcome> {
|
||||
let normalized = normalize_postgres_url(subname)?;
|
||||
let (ok, meta) = postgres::validate_postgres(&normalized).await?;
|
||||
let (ok, meta) = postgres::validate_postgres(&normalized, lax_tls).await?;
|
||||
|
||||
let mut message = if ok {
|
||||
"JDBC Postgres connection is valid.".to_string()
|
||||
|
|
|
|||
|
|
@ -12,17 +12,35 @@ use tokio::net::lookup_host;
|
|||
|
||||
use super::utils::check_url_resolvable;
|
||||
|
||||
/// One global, redirect-free client. Building a `Client` is comparatively
|
||||
/// expensive; re-using it lets reqwest share its internal connection pool
|
||||
/// and TLS sessions across JWT validations. `Lazy` ensures thread-safe,
|
||||
/// one-time initialisation.
|
||||
static NO_REDIRECT_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
/// Global redirect-free client with strict TLS validation.
|
||||
/// Building a `Client` is comparatively expensive; re-using it lets reqwest
|
||||
/// share its internal connection pool and TLS sessions across JWT validations.
|
||||
static STRICT_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
Client::builder()
|
||||
.redirect(Policy::none()) // disable all redirects
|
||||
.redirect(Policy::none())
|
||||
.danger_accept_invalid_certs(false)
|
||||
.build()
|
||||
.expect("failed to build no-redirect Client")
|
||||
.expect("failed to build strict Client")
|
||||
});
|
||||
|
||||
/// Global redirect-free client with lax TLS validation (accepts any cert).
|
||||
static LAX_CLIENT: Lazy<Client> = Lazy::new(|| {
|
||||
Client::builder()
|
||||
.redirect(Policy::none())
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("failed to build lax Client")
|
||||
});
|
||||
|
||||
/// Get the appropriate client based on TLS mode.
|
||||
fn get_client(lax_tls: bool) -> &'static Client {
|
||||
if lax_tls {
|
||||
&LAX_CLIENT
|
||||
} else {
|
||||
&STRICT_CLIENT
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 1918 + loopback + link-local nets we refuse to contact
|
||||
const BLOCKED_NETS: &[&str] = &[
|
||||
"10.0.0.0/8",
|
||||
|
|
@ -63,17 +81,32 @@ pub struct ValidateOptions {
|
|||
/// Backwards-compatible entry point with secure defaults:
|
||||
/// - `alg: none` is **rejected**
|
||||
/// - `iss` is **required** unless `fallback_decoding_key` is supplied (not supplied here)
|
||||
pub async fn validate_jwt(token: &str) -> Result<(bool, String)> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `token` - The JWT token to validate
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates for JWKS fetching
|
||||
pub async fn validate_jwt(token: &str, lax_tls: bool) -> Result<(bool, String)> {
|
||||
validate_jwt_with(
|
||||
token,
|
||||
&ValidateOptions { allow_alg_none: false, fallback_decoding_key: None },
|
||||
lax_tls,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Strict validator with policy control.
|
||||
/// Returns (is_active_credential, explanation).
|
||||
pub async fn validate_jwt_with(token: &str, opts: &ValidateOptions) -> Result<(bool, String)> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `token` - The JWT token to validate
|
||||
/// * `opts` - Validation options
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates for JWKS fetching
|
||||
pub async fn validate_jwt_with(
|
||||
token: &str,
|
||||
opts: &ValidateOptions,
|
||||
lax_tls: bool,
|
||||
) -> Result<(bool, String)> {
|
||||
let client = get_client(lax_tls);
|
||||
// --- insecure payload decode to read claims --------------------------------
|
||||
let claims: Claims = {
|
||||
let payload_b64 = token.split('.').nth(1).ok_or_else(|| anyhow!("invalid JWT format"))?;
|
||||
|
|
@ -169,7 +202,7 @@ pub async fn validate_jwt_with(token: &str, opts: &ValidateOptions) -> Result<(b
|
|||
|
||||
// build discovery URL and fetch it (redirects disabled)
|
||||
let config_url = format!("{}/.well-known/openid-configuration", issuer.trim_end_matches('/'));
|
||||
let cfg_resp = NO_REDIRECT_CLIENT
|
||||
let cfg_resp = client
|
||||
.get(&config_url)
|
||||
.send()
|
||||
.await
|
||||
|
|
@ -219,8 +252,7 @@ pub async fn validate_jwt_with(token: &str, opts: &ValidateOptions) -> Result<(b
|
|||
check_url_resolvable(&url).await.map_err(|e| anyhow!("jwks uri unresolvable: {e}"))?;
|
||||
|
||||
// fetch JWKS with redirect-free client
|
||||
let jwks_resp =
|
||||
NO_REDIRECT_CLIENT.get(url).send().await.map_err(|e| anyhow!("jwks fetch failed: {e}"))?;
|
||||
let jwks_resp = client.get(url).send().await.map_err(|e| anyhow!("jwks fetch failed: {e}"))?;
|
||||
if !jwks_resp.status().is_success() {
|
||||
return Ok((false, format!("jwks fetch failed: {}", jwks_resp.status())));
|
||||
}
|
||||
|
|
@ -293,7 +325,7 @@ mod tests {
|
|||
});
|
||||
|
||||
let token = encode(&header, &payload, &EncodingKey::from_secret(b"secret")).unwrap();
|
||||
let res = validate_jwt(&token).await.unwrap();
|
||||
let res = validate_jwt(&token, false).await.unwrap();
|
||||
assert!(!res.0);
|
||||
assert!(res.1.contains("HMAC-signed JWTs are not validated"));
|
||||
}
|
||||
|
|
@ -311,7 +343,7 @@ mod tests {
|
|||
let signature = URL_SAFE_NO_PAD.encode("sig");
|
||||
let token = format!("{header}.{payload}.{signature}");
|
||||
|
||||
let res = validate_jwt(&token).await.unwrap();
|
||||
let res = validate_jwt(&token, false).await.unwrap();
|
||||
assert!(!res.0);
|
||||
assert!(res.1.contains("no kid in header"));
|
||||
}
|
||||
|
|
@ -319,7 +351,7 @@ mod tests {
|
|||
#[tokio::test]
|
||||
async fn unsigned_token_rejected_by_default() {
|
||||
let token = build_unsigned_token(60);
|
||||
let res = validate_jwt(&token).await.unwrap();
|
||||
let res = validate_jwt(&token, false).await.unwrap();
|
||||
assert!(!res.0);
|
||||
assert!(res.1.contains("unsigned JWT (alg: none) not allowed"));
|
||||
}
|
||||
|
|
@ -330,6 +362,7 @@ mod tests {
|
|||
let res = validate_jwt_with(
|
||||
&token,
|
||||
&ValidateOptions { allow_alg_none: true, fallback_decoding_key: None },
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -342,6 +375,7 @@ mod tests {
|
|||
let res = validate_jwt_with(
|
||||
&token,
|
||||
&ValidateOptions { allow_alg_none: true, fallback_decoding_key: None },
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
|||
|
|
@ -3,8 +3,13 @@ use std::{net::IpAddr, time::Duration};
|
|||
|
||||
use anyhow::Result;
|
||||
use bson::doc;
|
||||
use mongodb::{error::ErrorKind, options::ClientOptions, Client};
|
||||
use mongodb::{
|
||||
error::ErrorKind,
|
||||
options::{ClientOptions, Tls, TlsOptions},
|
||||
Client,
|
||||
};
|
||||
use tokio::time::timeout;
|
||||
use tracing::debug;
|
||||
|
||||
pub fn looks_like_mongodb_uri(uri: &str) -> bool {
|
||||
// quick scheme check first
|
||||
|
|
@ -99,7 +104,11 @@ const SRV_SELECT_MS: u64 = 2500;
|
|||
|
||||
/// Validates a MongoDB URI in ≤ 2 s. Returns `(bool, String)` where the
|
||||
/// boolean indicates success and the string provides a status message.
|
||||
pub async fn validate_mongodb(uri: &str) -> Result<(bool, String)> {
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `uri` - The MongoDB connection URI to validate
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates
|
||||
pub async fn validate_mongodb(uri: &str, lax_tls: bool) -> Result<(bool, String)> {
|
||||
// ---- quick reject without touching the network
|
||||
if !looks_like_mongodb_uri(uri) {
|
||||
return Ok((false, "Invalid MongoDB URI".to_string()));
|
||||
|
|
@ -138,6 +147,13 @@ pub async fn validate_mongodb(uri: &str) -> Result<(bool, String)> {
|
|||
opts.max_pool_size = Some(1);
|
||||
opts.min_pool_size = Some(0);
|
||||
|
||||
// Configure TLS options based on lax_tls setting
|
||||
if lax_tls {
|
||||
debug!("Using lax TLS mode for MongoDB connection");
|
||||
let tls_options = TlsOptions::builder().allow_invalid_certificates(true).build();
|
||||
opts.tls = Some(Tls::Enabled(tls_options));
|
||||
}
|
||||
|
||||
// ---- dial and ping
|
||||
let client = Client::with_options(opts)?;
|
||||
let res = client.database("admin").run_command(doc! { "ping": 1 }).await;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{net::IpAddr, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use mysql_async::{prelude::Queryable, Conn, Opts, OptsBuilder};
|
||||
use mysql_async::{prelude::Queryable, Conn, Opts, OptsBuilder, SslOpts};
|
||||
use tokio::time::{error::Elapsed, timeout};
|
||||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
|
@ -94,7 +94,12 @@ fn targets_localhost(opts: &Opts) -> bool {
|
|||
is_local_host(opts.ip_or_hostname())
|
||||
}
|
||||
|
||||
pub async fn validate_mysql(mysql_url: &str) -> Result<(bool, Vec<String>)> {
|
||||
/// Validate a MySQL connection URL.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `mysql_url` - The MySQL connection URL to validate
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates
|
||||
pub async fn validate_mysql(mysql_url: &str, lax_tls: bool) -> Result<(bool, Vec<String>)> {
|
||||
let opts = parse_mysql_url(mysql_url)?;
|
||||
|
||||
if targets_localhost(&opts) {
|
||||
|
|
@ -102,7 +107,15 @@ pub async fn validate_mysql(mysql_url: &str) -> Result<(bool, Vec<String>)> {
|
|||
return Ok((false, vec!["skipped localhost/loopback host".into()]));
|
||||
}
|
||||
|
||||
let builder = OptsBuilder::from_opts(opts).stmt_cache_size(Some(0));
|
||||
let mut builder = OptsBuilder::from_opts(opts).stmt_cache_size(Some(0));
|
||||
|
||||
// Configure TLS options based on lax_tls setting
|
||||
if lax_tls {
|
||||
debug!("Using lax TLS mode for MySQL connection");
|
||||
let ssl_opts = SslOpts::default().with_danger_accept_invalid_certs(true);
|
||||
builder = builder.ssl_opts(Some(ssl_opts));
|
||||
}
|
||||
|
||||
let opts: Opts = builder.into();
|
||||
|
||||
let host = opts.ip_or_hostname().to_string();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
use std::{str::FromStr, sync::Once, time::Duration};
|
||||
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use rustls::crypto::{ring, CryptoProvider};
|
||||
use rustls::{client::ClientConfig, RootCertStore};
|
||||
use once_cell::sync::OnceCell;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::crypto::{ring, verify_tls12_signature, verify_tls13_signature, CryptoProvider};
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::{client::ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme};
|
||||
use rustls_native_certs::{load_native_certs, CertificateResult};
|
||||
use sha1::{Digest, Sha1};
|
||||
use tokio::time::{error::Elapsed, timeout};
|
||||
|
|
@ -16,15 +19,58 @@ use tracing::debug;
|
|||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
static INIT_PROVIDER: Once = Once::new();
|
||||
static INIT_PROVIDER: OnceCell<()> = OnceCell::new();
|
||||
fn ensure_crypto_provider() {
|
||||
INIT_PROVIDER.call_once(|| {
|
||||
INIT_PROVIDER.get_or_init(|| {
|
||||
// If another part of the program already installed a provider,
|
||||
// ignore the error — we just need one global provider.
|
||||
let _ = CryptoProvider::install_default(ring::default_provider());
|
||||
});
|
||||
}
|
||||
|
||||
/// A certificate verifier that accepts any certificate (for lax TLS mode).
|
||||
///
|
||||
/// This verifier still validates signatures to ensure the connection is encrypted,
|
||||
/// but does not verify the certificate chain against trusted CAs.
|
||||
#[derive(Debug)]
|
||||
struct LaxCertVerifier(Arc<CryptoProvider>);
|
||||
|
||||
impl ServerCertVerifier for LaxCertVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> std::result::Result<ServerCertVerified, rustls::Error> {
|
||||
// Accept any certificate - this is the "lax" behavior
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.0.signature_verification_algorithms.supported_schemes()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_postgres_cache_key(postgres_url: &str) -> String {
|
||||
let mut hasher = Sha1::new();
|
||||
hasher.update(postgres_url.as_bytes());
|
||||
|
|
@ -46,7 +92,12 @@ pub fn parse_postgres_url(postgres_url: &str) -> Result<Config> {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn validate_postgres(postgres_url: &str) -> Result<(bool, Vec<String>)> {
|
||||
/// Validate a Postgres connection URL.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `postgres_url` - The Postgres connection URL to validate
|
||||
/// * `lax_tls` - If true, accept self-signed or invalid certificates
|
||||
pub async fn validate_postgres(postgres_url: &str, lax_tls: bool) -> Result<(bool, Vec<String>)> {
|
||||
let mut cfg = parse_postgres_url(postgres_url)?;
|
||||
|
||||
// --- skip localhost/loopback/unix-socket targets entirely -------------
|
||||
|
|
@ -60,7 +111,7 @@ pub async fn validate_postgres(postgres_url: &str) -> Result<(bool, Vec<String>)
|
|||
cfg.ssl_mode(SslMode::Disable);
|
||||
}
|
||||
|
||||
check_postgres_db_connection(cfg, original_mode).await
|
||||
check_postgres_db_connection(cfg, original_mode, lax_tls).await
|
||||
}
|
||||
|
||||
fn has_any_local_host(cfg: &Config) -> bool {
|
||||
|
|
@ -98,6 +149,7 @@ fn is_local_tcp_host(s: &str) -> bool {
|
|||
async fn check_postgres_db_connection(
|
||||
mut cfg: Config,
|
||||
original_mode: SslMode,
|
||||
lax_tls: bool,
|
||||
) -> Result<(bool, Vec<String>)> {
|
||||
// First attempt with caller-supplied sslmode, optional retry without TLS.
|
||||
for attempt in 0..=1 {
|
||||
|
|
@ -121,16 +173,26 @@ async fn check_postgres_db_connection(
|
|||
// Ensure Rustls crypto provider is installed *before* using the builder
|
||||
ensure_crypto_provider();
|
||||
|
||||
let CertificateResult { certs, errors, .. } = load_native_certs();
|
||||
for err in errors {
|
||||
debug!("native-cert error: {err}");
|
||||
}
|
||||
let tls_cfg = if lax_tls {
|
||||
// Lax mode: accept any certificate (self-signed, expired, wrong hostname)
|
||||
debug!("Using lax TLS mode for Postgres connection");
|
||||
let provider = Arc::new(ring::default_provider());
|
||||
ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(LaxCertVerifier(provider)))
|
||||
.with_no_client_auth()
|
||||
} else {
|
||||
// Strict mode: full certificate validation
|
||||
let CertificateResult { certs, errors, .. } = load_native_certs();
|
||||
for err in errors {
|
||||
debug!("native-cert error: {err}");
|
||||
}
|
||||
|
||||
let mut roots = RootCertStore::empty();
|
||||
let _ = roots.add_parsable_certificates(certs);
|
||||
let mut roots = RootCertStore::empty();
|
||||
let _ = roots.add_parsable_certificates(certs);
|
||||
|
||||
let tls_cfg =
|
||||
ClientConfig::builder().with_root_certificates(roots).with_no_client_auth();
|
||||
ClientConfig::builder().with_root_certificates(roots).with_no_client_auth()
|
||||
};
|
||||
let tls = MakeRustlsConnect::new(tls_cfg);
|
||||
|
||||
let (client, connection) = cfg_try.connect(tls).await?;
|
||||
|
|
|
|||
1039
tests/cli_validate_revoke.rs
Normal file
1039
tests/cli_validate_revoke.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -27,6 +27,7 @@ fn make_rule(rule_id: &str, depends_on_rule: Vec<Option<DependsOnRule>>) -> Arc<
|
|||
revocation: None,
|
||||
depends_on_rule,
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +58,7 @@ fn make_match(rule: Arc<Rule>, blob_id: BlobId, value: &str) -> Match {
|
|||
calculated_entropy: 0.0,
|
||||
visible: true,
|
||||
is_base64: false,
|
||||
dependent_captures: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ fn make_match(fp: u64, rule_id: &str) -> Match {
|
|||
revocation: None,
|
||||
depends_on_rule: vec![],
|
||||
pattern_requirements: None,
|
||||
tls_mode: None,
|
||||
};
|
||||
let rule = Arc::new(Rule::new(syntax));
|
||||
Match {
|
||||
|
|
@ -63,6 +64,7 @@ fn make_match(fp: u64, rule_id: &str) -> Match {
|
|||
calculated_entropy: 0.0,
|
||||
visible: true,
|
||||
is_base64: false,
|
||||
dependent_captures: std::collections::BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -170,6 +170,7 @@ fn run_skiplist(skip_regex: Vec<String>, skip_skipword: Vec<String>) -> Result<u
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules).load(&scan_args)?;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -170,6 +170,7 @@ fn test_bitbucket_remote_scan() -> Result<()> {
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -190,6 +190,7 @@ rules:
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
// ── load rules once ─────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -177,6 +177,7 @@ fn test_github_remote_scan() -> Result<()> {
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
// Create in-memory datastore
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -175,6 +175,7 @@ fn test_gitlab_remote_scan() -> Result<()> {
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
@ -338,6 +339,7 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::{GlobalArgs, Mode},
|
||||
global::{GlobalArgs, Mode, TlsMode},
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
rule_loader::RuleLoader,
|
||||
|
|
@ -153,6 +153,7 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
|
|||
progress: Mode::Never,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules).load(&scan_args)?;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -308,6 +308,7 @@ async fn test_scan_slack_messages() -> Result<()> {
|
|||
progress: Mode::Never,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -251,6 +251,7 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
|
|||
self_update: false,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
let update_status = UpdateStatus::default();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use kingfisher::{
|
|||
rules::RuleSpecifierArgs,
|
||||
scan::{ConfidenceLevel, ScanArgs},
|
||||
},
|
||||
global::Mode,
|
||||
global::{Mode, TlsMode},
|
||||
GlobalArgs,
|
||||
},
|
||||
findings_store::FindingsStore,
|
||||
|
|
@ -313,6 +313,7 @@ impl TestContext {
|
|||
progress: Mode::Never,
|
||||
ignore_certs: false,
|
||||
user_agent_suffix: None,
|
||||
tls_mode: TlsMode::Strict,
|
||||
};
|
||||
|
||||
let datastore = Arc::new(Mutex::new(FindingsStore::new(clone_dir)));
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ async fn validates_mysql_secret_against_testcontainer() -> Result<()> {
|
|||
wait_for_port(HOST_ALIAS, port).await?;
|
||||
|
||||
let uri = format!("mysql://root:secret@{HOST_ALIAS}:{port}/app");
|
||||
let (is_valid, metadata) = validate_mysql(&uri).await?;
|
||||
let (is_valid, metadata) = validate_mysql(&uri, false).await?;
|
||||
|
||||
assert!(is_valid, "expected MySQL validation to succeed, got {metadata:?}");
|
||||
assert!(
|
||||
|
|
@ -85,7 +85,7 @@ async fn validates_postgres_secret_against_testcontainer() -> Result<()> {
|
|||
wait_for_port(HOST_ALIAS, port).await?;
|
||||
|
||||
let uri = format!("postgres://postgres:secret@{HOST_ALIAS}:{port}/postgres");
|
||||
let (is_valid, metadata) = validate_postgres(&uri).await?;
|
||||
let (is_valid, metadata) = validate_postgres(&uri, false).await?;
|
||||
|
||||
assert!(is_valid, "expected Postgres validation to succeed");
|
||||
assert!(metadata.is_empty(), "expected no metadata but found {metadata:?}");
|
||||
|
|
|
|||
200
tests/tls_mode.rs
Normal file
200
tests/tls_mode.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
//! Tests for the `--tls-mode` CLI feature and TLS validation behavior.
|
||||
//!
|
||||
//! These tests verify that:
|
||||
//! - The `--tls-mode` CLI flag is parsed correctly
|
||||
//! - The `--ignore-certs` legacy flag is treated as `--tls-mode=off`
|
||||
//! - Rules with `tls_mode: lax` are correctly parsed and respected
|
||||
//! - The TLS mode behavior works as expected for different validators
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
|
||||
/// Test that `--tls-mode` is recognized as a valid global option.
|
||||
#[test]
|
||||
fn tls_mode_flag_is_recognized() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=strict").arg("--help");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
/// Test that all TLS mode values are accepted.
|
||||
#[test]
|
||||
fn tls_mode_accepts_all_values() {
|
||||
for mode in ["strict", "lax", "off"] {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg(format!("--tls-mode={}", mode)).arg("--help");
|
||||
cmd.assert().success();
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that invalid TLS mode values are rejected.
|
||||
#[test]
|
||||
fn tls_mode_rejects_invalid_values() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=invalid").arg("--help");
|
||||
cmd.assert().failure().stderr(predicate::str::contains("invalid"));
|
||||
}
|
||||
|
||||
/// Test that `--ignore-certs` is still accepted (deprecated but supported).
|
||||
#[test]
|
||||
fn ignore_certs_flag_still_works() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--ignore-certs").arg("--help");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
/// Test that --tls-mode appears in the help output.
|
||||
#[test]
|
||||
fn tls_mode_appears_in_help() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--help");
|
||||
cmd.assert().success().stdout(predicate::str::contains("--tls-mode"));
|
||||
}
|
||||
|
||||
/// Test that rules list subcommand runs with tls-mode flag.
|
||||
#[test]
|
||||
fn rules_list_works_with_tls_mode() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=lax").arg("rules").arg("list");
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("postgres").or(predicate::str::contains("Postgres")));
|
||||
}
|
||||
|
||||
/// Test that a scan with `--tls-mode=strict` runs successfully.
|
||||
#[test]
|
||||
fn scan_with_strict_mode_runs() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=strict").arg("scan").arg("--no-validate").arg("-");
|
||||
cmd.write_stdin("test input with no secrets");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
/// Test that a scan with `--tls-mode=lax` runs successfully.
|
||||
#[test]
|
||||
fn scan_with_lax_mode_runs() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=lax").arg("scan").arg("--no-validate").arg("-");
|
||||
cmd.write_stdin("test input with no secrets");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
/// Test that a scan with `--tls-mode=off` runs successfully.
|
||||
#[test]
|
||||
fn scan_with_off_mode_runs() {
|
||||
let mut cmd = Command::cargo_bin("kingfisher").unwrap();
|
||||
cmd.arg("--tls-mode=off").arg("scan").arg("--no-validate").arg("-");
|
||||
cmd.write_stdin("test input with no secrets");
|
||||
cmd.assert().success();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod rule_tls_mode_tests {
|
||||
use kingfisher_rules::{RuleSyntax, TlsMode};
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Helper struct for deserializing rule YAML files.
|
||||
#[derive(Deserialize)]
|
||||
struct RawRules {
|
||||
rules: Vec<RuleSyntax>,
|
||||
}
|
||||
|
||||
/// Test that the postgres rule has tls_mode: lax.
|
||||
#[test]
|
||||
fn postgres_rule_has_lax_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/postgres.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("postgres rules should parse");
|
||||
|
||||
let postgres_rule = raw.rules.iter().find(|r| r.id == "kingfisher.postgres.1");
|
||||
assert!(postgres_rule.is_some(), "postgres rule should exist");
|
||||
assert_eq!(
|
||||
postgres_rule.unwrap().tls_mode,
|
||||
Some(TlsMode::Lax),
|
||||
"postgres rule should have tls_mode: lax"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the mysql rule has tls_mode: lax.
|
||||
#[test]
|
||||
fn mysql_rule_has_lax_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/mysql.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("mysql rules should parse");
|
||||
|
||||
let mysql_rule = raw.rules.iter().find(|r| r.id == "kingfisher.mysql.1");
|
||||
assert!(mysql_rule.is_some(), "mysql rule should exist");
|
||||
assert_eq!(
|
||||
mysql_rule.unwrap().tls_mode,
|
||||
Some(TlsMode::Lax),
|
||||
"mysql rule should have tls_mode: lax"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the mongodb URI rule has tls_mode: lax.
|
||||
#[test]
|
||||
fn mongodb_uri_rule_has_lax_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/mongodb.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("mongodb rules should parse");
|
||||
|
||||
let mongodb_rule = raw.rules.iter().find(|r| r.id == "kingfisher.mongodb.3");
|
||||
assert!(mongodb_rule.is_some(), "mongodb.3 rule should exist");
|
||||
assert_eq!(
|
||||
mongodb_rule.unwrap().tls_mode,
|
||||
Some(TlsMode::Lax),
|
||||
"mongodb.3 rule should have tls_mode: lax"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the jdbc rule has tls_mode: lax.
|
||||
#[test]
|
||||
fn jdbc_rule_has_lax_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/jdbc.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("jdbc rules should parse");
|
||||
|
||||
let jdbc_rule = raw.rules.iter().find(|r| r.id == "kingfisher.jdbc.1");
|
||||
assert!(jdbc_rule.is_some(), "jdbc rule should exist");
|
||||
assert_eq!(
|
||||
jdbc_rule.unwrap().tls_mode,
|
||||
Some(TlsMode::Lax),
|
||||
"jdbc rule should have tls_mode: lax"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that the jwt rule has tls_mode: lax.
|
||||
#[test]
|
||||
fn jwt_rule_has_lax_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/jwt.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("jwt rules should parse");
|
||||
|
||||
let jwt_rule = raw.rules.iter().find(|r| r.id == "kingfisher.jwt.1");
|
||||
assert!(jwt_rule.is_some(), "jwt rule should exist");
|
||||
assert_eq!(
|
||||
jwt_rule.unwrap().tls_mode,
|
||||
Some(TlsMode::Lax),
|
||||
"jwt rule should have tls_mode: lax"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that rules without tls_mode (e.g., SaaS APIs) have None.
|
||||
#[test]
|
||||
fn github_rule_has_no_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/github.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("github rules should parse");
|
||||
|
||||
// GitHub rules should not have tls_mode set (SaaS API, always strict)
|
||||
for rule in &raw.rules {
|
||||
assert_eq!(rule.tls_mode, None, "github rule {} should not have tls_mode set", rule.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that rules without tls_mode (e.g., SaaS APIs) have None.
|
||||
#[test]
|
||||
fn aws_rule_has_no_tls_mode() {
|
||||
let yaml = include_str!("../crates/kingfisher-rules/data/rules/aws.yml");
|
||||
let raw: RawRules = serde_yaml::from_str(yaml).expect("aws rules should parse");
|
||||
|
||||
// AWS rules should not have tls_mode set (SaaS API, always strict)
|
||||
for rule in &raw.rules {
|
||||
assert_eq!(rule.tls_mode, None, "aws rule {} should not have tls_mode set", rule.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue