This commit is contained in:
Mick Grove 2026-02-10 19:43:34 -08:00
commit 4a74e95756
10 changed files with 103 additions and 53 deletions

View file

@ -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:

View file

@ -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) ────────────────

View file

@ -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; \

View file

@ -35,7 +35,7 @@ Designed for offensive security engineers and blue-teamers alike, Kingfisher hel
### Performance, Accuracy, and Hundreds of Rules
- **Performance**: multithreaded, Hyperscanpowered 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

View file

@ -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
# - 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

View file

@ -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"']

View file

@ -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(&[]);

View file

@ -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<RootCertStore> {
Ok(roots)
}
fn cached_h2_tls_config() -> Result<Arc<ClientConfig>> {
static TLS_CONFIG: OnceCell<Arc<ClientConfig>> = OnceCell::new();
let cfg = TLS_CONFIG.get_or_try_init(|| -> Result<Arc<ClientConfig>> {
// 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<Uri> {
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}"))?;

View file

@ -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;
}
}
}

View file

@ -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);