Refactored into multiple crates. Added the 'validate' subcommand

This commit is contained in:
Mick Grove 2026-01-28 22:24:35 -08:00
commit 1c45efde3e
4 changed files with 42 additions and 21 deletions

View file

@ -636,37 +636,39 @@ This is useful for:
- Checking if a credential is still active before rotation
- Validating secrets from external sources (password managers, ticketing systems, etc.)
> **Note:** The `kingfisher.` prefix is optional for built-in rules. You can use `--rule aws` instead of `--rule kingfisher.aws`.
```bash
# Validate an OpsGenie API key (using rule prefix matching)
kingfisher validate --rule kingfisher.opsgenie "12345678-9abc-def0-1234-56789abcdef0"
kingfisher validate --rule opsgenie "12345678-9abc-def0-1234-56789abcdef0"
# Validate from stdin
echo "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | kingfisher validate --rule kingfisher.github -
echo "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | kingfisher validate --rule github -
# JSON output for scripting
kingfisher validate --rule kingfisher.slack "xoxb-..." --format json
kingfisher validate --rule slack "xoxb-..." --format json
# AWS credentials - use --arg to auto-assign additional values
kingfisher validate --rule kingfisher.aws --arg AKIAIOSFODNN7EXAMPLE \
kingfisher validate --rule aws --arg AKIAIOSFODNN7EXAMPLE \
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Or use --var if you know the variable name
# Or use --var if you know the variable name (explicit rule ID still works)
kingfisher validate --rule kingfisher.aws.2 --var AKID=AKIAIOSFODNN7EXAMPLE \
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# GCP service account (pass JSON as secret)
kingfisher validate --rule kingfisher.gcp "$(cat service-account.json)"
kingfisher validate --rule gcp "$(cat service-account.json)"
# MongoDB connection string
kingfisher validate --rule kingfisher.mongodb.3 \
kingfisher validate --rule mongodb.3 \
"mongodb+srv://user:password@cluster.mongodb.net/db"
# PostgreSQL connection
kingfisher validate --rule kingfisher.postgres \
kingfisher validate --rule postgres \
"postgres://admin:password@db.example.com:5432/mydb"
# JWT token
kingfisher validate --rule kingfisher.jwt \
kingfisher validate --rule jwt \
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
```
@ -683,16 +685,16 @@ Some validators need more than just the secret. For example, AWS needs both an a
```bash
# --arg auto-assigns to AKID (the only non-TOKEN variable for AWS)
kingfisher validate --rule kingfisher.aws --arg AKIAEXAMPLE "secret_key"
kingfisher validate --rule aws --arg AKIAEXAMPLE "secret_key"
# --var for explicit assignment
kingfisher validate --rule kingfisher.aws --var AKID=AKIAEXAMPLE "secret_key"
kingfisher validate --rule aws --var AKID=AKIAEXAMPLE "secret_key"
```
**Rule prefix matching:** Use partial rule IDs like `kingfisher.opsgenie` instead of the full `kingfisher.opsgenie.1`. If the prefix matches multiple rules, **all matching rules with compatible variables are tried**:
**Rule prefix matching:** Use partial rule IDs like `opsgenie` instead of the full `kingfisher.opsgenie.1`. If the prefix matches multiple rules, **all matching rules with compatible variables are tried**:
```bash
$ kingfisher validate --rule kingfisher.aws --arg AKIAEXAMPLE "secret_key"
$ kingfisher validate --rule aws --arg AKIAEXAMPLE "secret_key"
Rule: AWS Secret Access Key (kingfisher.aws.2)
Result: ✓ VALID
Response: arn:aws:iam::123456789012:user/example

View file

@ -4,7 +4,8 @@ use std::path::PathBuf;
/// Directly validate a known secret against a rule's validator
#[derive(Args, Debug, Clone)]
pub struct ValidateArgs {
/// Rule ID or prefix to use for validation (e.g., kingfisher.opsgenie.1 or kingfisher.opsgenie)
/// Rule ID or prefix to use for validation (e.g., aws, opsgenie, or kingfisher.aws.2).
/// The `kingfisher.` prefix is optional for built-in rules.
#[arg(long, required = true)]
pub rule: String,

View file

@ -64,12 +64,30 @@ fn find_rules_by_selector<'a>(
) -> Result<Vec<&'a Rule>> {
let mut matches: Vec<&Rule> = Vec::new();
for (id, rule) in rules {
// Exact match OR "selector." is a prefix of id
if id == selector
|| (id.starts_with(selector) && id.as_bytes().get(selector.len()) == Some(&b'.'))
{
matches.push(rule);
// Try the selector as-is first, then with "kingfisher." prefix as fallback.
// This allows users to pass `--rule aws` instead of `--rule kingfisher.aws`.
let selectors_to_try: Vec<std::borrow::Cow<'_, str>> = if selector.starts_with("kingfisher.") {
vec![std::borrow::Cow::Borrowed(selector)]
} else {
vec![
std::borrow::Cow::Borrowed(selector),
std::borrow::Cow::Owned(format!("kingfisher.{}", selector)),
]
};
for try_selector in &selectors_to_try {
for (id, rule) in rules {
// Exact match OR "selector." is a prefix of id
if id == try_selector.as_ref()
|| (id.starts_with(try_selector.as_ref())
&& id.as_bytes().get(try_selector.len()) == Some(&b'.'))
{
matches.push(rule);
}
}
// If we matched with this selector, no need to try the fallback
if !matches.is_empty() {
break;
}
}

View file

@ -139,7 +139,7 @@ impl LoadedRules {
let mut matched_any = false;
for (id, rule) in &self.id_to_rule {
// Exact match OR “selector.” is a prefix of id
// Exact match OR "selector." is a prefix of id
if id == selector
|| (id.starts_with(selector)
&& id.as_bytes().get(selector.len()) == Some(&b'.'))