diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efc560a..819b8a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ env: VCPKG_DOWNLOADS: C:\vcpkg\downloads VCPKG_FEATURE_FLAGS: binarycaching VCPKG_BINARY_SOURCES: clear;x-gha,readwrite - RUST_TOOLCHAIN: "1.90" + RUST_TOOLCHAIN: "1.92" jobs: linux-arm64: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f303d66..b80f8b7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ env: VCPKG_DOWNLOADS: C:\vcpkg\downloads VCPKG_FEATURE_FLAGS: binarycaching VCPKG_BINARY_SOURCES: clear;x-gha,readwrite - RUST_TOOLCHAIN: "1.90" + RUST_TOOLCHAIN: "1.92" jobs: # ──────────────── Linux (via Makefile) ──────────────── diff --git a/Makefile b/Makefile index b41b470..b9b69ff 100644 --- a/Makefile +++ b/Makefile @@ -110,11 +110,11 @@ setup-zig: ubuntu-x64: setup-zig # ensures Zig & cargo-zigbuild exist @echo "Checking Rust toolchain…" @$(MAKE) check-rust || { \ - echo "🦀 Installing Rust 1.90.0 …"; \ + echo "🦀 Installing Rust 1.92.0 …"; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \ . $$HOME/.cargo/env; \ - rustup toolchain install 1.90.0; \ - rustup default 1.90.0; \ + rustup toolchain install 1.92.0; \ + rustup default 1.92.0; \ } @echo "📦 Installing build dependencies (musl, cmake, etc.)…" @@ -150,11 +150,11 @@ ubuntu-x64: setup-zig # ensures Zig & cargo-zigbuild exist ubuntu-arm64: setup-zig # ensures Zig & cargo-zigbuild exist @echo "Checking Rust toolchain…" @$(MAKE) check-rust || { \ - echo "🦀 Installing Rust 1.90.0 …"; \ + echo "🦀 Installing Rust 1.92.0 …"; \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y; \ . $$HOME/.cargo/env; \ - rustup toolchain install 1.90.0; \ - rustup default 1.90.0; \ + rustup toolchain install 1.92.0; \ + rustup default 1.92.0; \ } @echo "📦 Installing build dependencies (musl, cmake, etc.)…" @@ -248,7 +248,7 @@ endif linux-x64: check-docker create-dockerignore @mkdir -p target/release docker run --platform linux/amd64 --rm \ - -v "$$(pwd):/src" -w /src rust:1.90-alpine sh -eu -c '\ + -v "$$(pwd):/src" -w /src rust:1.92-alpine sh -eu -c '\ apk add --no-cache \ musl-dev \ gcc g++ make cmake pkgconfig \ @@ -277,7 +277,7 @@ linux-x64: check-docker create-dockerignore linux-arm64: check-docker create-dockerignore @mkdir -p target/release docker run --platform linux/arm64 --rm \ - -v "$$(pwd):/src" -w /src rust:1.90-alpine sh -eu -c '\ + -v "$$(pwd):/src" -w /src rust:1.92-alpine sh -eu -c '\ apk add --no-cache \ musl-dev \ gcc g++ make cmake pkgconfig \ @@ -388,7 +388,7 @@ check-rust: echo "Rust not found."; \ exit 1; \ fi; \ - required=1.90.0; \ + required=1.92.0; \ if [ $$(printf '%s\n' "$$required" "$$version" | sort -V | head -n1) != "$$required" ]; then \ echo "Rust version $$version is older than required $$required."; \ exit 1; \ diff --git a/README.md b/README.md index d3364ab..7c3aa29 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Designed for offensive security engineers and blue-teamers alike, Kingfisher hel ### Performance, Accuracy, and Hundreds of Rules - **Performance**: multithreaded, Hyperscan‑powered scanning built for huge codebases - **Extensible rules**: hundreds of built-in detectors plus YAML-defined custom rules ([docs/RULES.md](/docs/RULES.md)) -- **Validate & Revoke**: live validation of discovered secrets, plus direct revocation for supported platforms (GitHub, GitLab, Slack, AWS, GCP, and more)[docs/USAGE.md](/docs/USAGE.md)) +- **Validate & Revoke**: live validation of discovered secrets, plus direct revocation for supported platforms (GitHub, GitLab, Slack, AWS, GCP, and more) ([docs/USAGE.md](/docs/USAGE.md)) - **Blast Radius Mapping**: instantly map leaked keys to their effective cloud identities and exposed resources with `--access-map`. Supports AWS, GCP, Azure, GitHub, Gitlab, and more token support coming. - **Broad AI SaaS coverage**: finds and validates tokens for OpenAI, Anthropic, Google Gemini, Cohere, AWS Bedrock, Voyage AI, Mistral, Stability AI, Replicate, xAI (Grok), Ollama, Langchain, Perplexity, Weights & Biases, Cerebras, Friendli, Fireworks.ai, NVIDIA NIM, Together.ai, Zhipu, and many more - **Compressed Files**: Supports extracting and scanning compressed files for secrets diff --git a/crates/kingfisher-rules/data/rules/modal.yml b/crates/kingfisher-rules/data/rules/modal.yml index 695ed78..9484534 100644 --- a/crates/kingfisher-rules/data/rules/modal.yml +++ b/crates/kingfisher-rules/data/rules/modal.yml @@ -47,22 +47,22 @@ rules: header: grpc-status expected: ["0"] - - name: Modal Token Secret - id: kingfisher.modal.2 - pattern: | - (?x) - \b - ( - as-[A-Za-z0-9]{22} - ) - \b - pattern_requirements: - min_digits: 2 - min_entropy: 3.0 - confidence: medium - examples: - - "as-aB1cD2eF3gH4iJ5kL6mN7P" - references: - - https://modal.com/docs/reference/cli/token - - https://modal.com/docs/reference/modal.Client - - https://modal.com/docs/reference/modal.App \ No newline at end of file + # - name: Modal Token Secret + # id: kingfisher.modal.2 + # pattern: | + # (?x) + # \b + # ( + # as-[A-Za-z0-9]{22} + # ) + # \b + # pattern_requirements: + # min_digits: 2 + # min_entropy: 3.0 + # confidence: medium + # examples: + # - "as-aB1cD2eF3gH4iJ5kL6mN7P" + # references: + # - https://modal.com/docs/reference/cli/token + # - https://modal.com/docs/reference/modal.Client + # - https://modal.com/docs/reference/modal.App \ No newline at end of file diff --git a/crates/kingfisher-rules/data/rules/owlbot.yml b/crates/kingfisher-rules/data/rules/owlbot.yml index 6fafe10..bdc033c 100644 --- a/crates/kingfisher-rules/data/rules/owlbot.yml +++ b/crates/kingfisher-rules/data/rules/owlbot.yml @@ -27,13 +27,14 @@ rules: content: request: method: GET - url: "https://owlbot.info/api/v4/dictionary/owl?format=json" + url: "https://www.owlbot.ai/api/login/checkToken" headers: - Authorization: "Token {{ TOKEN }}" + # Owlbot expects the API key directly in `Authorization`. + Authorization: "{{ TOKEN }}" Accept: application/json response_matcher: - report_response: true - type: StatusMatch status: [200] - type: WordMatch - words: ['"word"', '"definitions"'] + words: ['"user"', '"chatbot"'] diff --git a/src/direct_validate.rs b/src/direct_validate.rs index d05640d..95570c2 100644 --- a/src/direct_validate.rs +++ b/src/direct_validate.rs @@ -43,6 +43,20 @@ use crate::{ use crate::grpc_validation; +fn preview_body_for_display(body: &str, max_bytes: usize) -> String { + if body.len() <= max_bytes { + return body.to_string(); + } + + // `String` slicing must be on a UTF-8 char boundary to avoid panics. + let mut end = max_bytes.min(body.len()); + while end > 0 && !body.is_char_boundary(end) { + end -= 1; + } + + format!("{}...", &body[..end]) +} + /// Result of a direct validation attempt. #[derive(Debug, Clone, Serialize)] pub struct DirectValidationResult { @@ -314,7 +328,7 @@ async fn execute_http_validation( response.text().await.unwrap_or_else(|e| format!("Failed to read response body: {}", e)); // Truncate body for display if too long - let display_body = if body.len() > 500 { format!("{}...", &body[..500]) } else { body.clone() }; + let display_body = preview_body_for_display(&body, 500); // Validate the response let matchers = http_validation.request.response_matcher.as_deref().unwrap_or(&[]); @@ -369,7 +383,7 @@ async fn execute_grpc_validation( } // Truncate body for display if too long - let display_body = if body.len() > 500 { format!("{}...", &body[..500]) } else { body.clone() }; + let display_body = preview_body_for_display(&body, 500); // Validate the response let matchers = grpc_validation_cfg.request.response_matcher.as_deref().unwrap_or(&[]); diff --git a/src/grpc_validation.rs b/src/grpc_validation.rs index 8d20b2f..1e98f9f 100644 --- a/src/grpc_validation.rs +++ b/src/grpc_validation.rs @@ -5,6 +5,7 @@ use bytes::Bytes; use h2::client; use http::{header::HeaderName, HeaderMap, HeaderValue, Request, Uri}; use liquid::Object; +use once_cell::sync::OnceCell; use reqwest::Url; use rustls::{ClientConfig, RootCertStore}; use tokio::net::TcpStream; @@ -31,6 +32,21 @@ fn build_root_store() -> Result { Ok(roots) } +fn cached_h2_tls_config() -> Result> { + static TLS_CONFIG: OnceCell> = OnceCell::new(); + + let cfg = TLS_CONFIG.get_or_try_init(|| -> Result> { + // Loading native roots can be relatively expensive; do it once and reuse. + let mut cfg = ClientConfig::builder() + .with_root_certificates(build_root_store()?) + .with_no_client_auth(); + cfg.alpn_protocols = vec![b"h2".to_vec()]; + Ok(Arc::new(cfg)) + })?; + + Ok(Arc::clone(cfg)) +} + fn url_to_h2_uri(url: &Url) -> Result { let scheme = url.scheme(); if scheme != "https" { @@ -94,11 +110,7 @@ pub async fn grpc_unary_call( .context("Timed out connecting to gRPC host")? .context("Failed to connect to gRPC host")?; - let mut tls_config = - ClientConfig::builder().with_root_certificates(build_root_store()?).with_no_client_auth(); - tls_config.alpn_protocols = vec![b"h2".to_vec()]; - - let connector = TlsConnector::from(Arc::new(tls_config)); + let connector = TlsConnector::from(cached_h2_tls_config()?); let server_name = rustls::pki_types::ServerName::try_from(host.to_string()) .map_err(|_| anyhow!("Invalid TLS server name: {host}"))?; diff --git a/src/main.rs b/src/main.rs index 08f6580..de79caa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,14 @@ fn main() -> anyhow::Result<()> { color_backtrace::install(); // Rustls 0.23 requires an explicit crypto provider selection when multiple // providers are present in the dependency graph. - let _ = rustls::crypto::ring::default_provider().install_default(); + match rustls::crypto::ring::default_provider().install_default() { + Ok(()) => {} + Err(_already_installed) => { + // Another crate already installed a provider. This is unusual for a CLI, but + // surfacing it makes later TLS issues much easier to diagnose. + warn!("rustls crypto provider was already installed; keeping existing provider"); + } + } // Parse command-line arguments let CommandLineArgs { command, global_args } = CommandLineArgs::parse_args(); @@ -687,7 +694,7 @@ pub fn run_rules_check(args: &RulesCheckArgs) -> Result<()> { matched_term ); println!(" Example: {}", example); - num_errors += 1; + num_warnings += 1; } } } diff --git a/src/validation.rs b/src/validation.rs index 80dbccd..b3ab95e 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -693,11 +693,19 @@ async fn timed_validate_single_match<'a>( m.validation_response_status = status; let body_opt = validation_body::from_string(body.clone()); m.validation_response_body = body_opt.clone(); - let matchers = http_validation - .request - .response_matcher - .as_ref() - .expect("missing response_matcher"); + let matchers = match http_validation.request.response_matcher.as_ref() { + Some(m) => m, + None => { + m.validation_success = false; + m.validation_response_body = validation_body::from_string(format!( + "HTTP validation for rule '{}' is missing `response_matcher`", + rule_syntax.name + )); + m.validation_response_status = StatusCode::BAD_REQUEST; + commit_and_return(m); + return; + } + }; m.validation_success = httpvalidation::validate_response( matchers, @@ -799,11 +807,19 @@ async fn timed_validate_single_match<'a>( m.validation_response_status = status; m.validation_response_body = validation_body::from_string(body.clone()); - let matchers = grpc_validation_cfg - .request - .response_matcher - .as_ref() - .expect("missing response_matcher"); + let matchers = match grpc_validation_cfg.request.response_matcher.as_ref() { + Some(m) => m, + None => { + m.validation_success = false; + m.validation_response_body = validation_body::from_string(format!( + "gRPC validation for rule '{}' is missing `response_matcher`", + rule_syntax.name + )); + m.validation_response_status = StatusCode::BAD_REQUEST; + commit_and_return(m); + return; + } + }; m.validation_success = httpvalidation::validate_response(matchers, &body, &status, &headers, false);