forked from mirrors/kingfisher
added multi-step revocation support. Added revocation support for SendGrid, Netlify, Tailscale, ElevenLabs, Sourcegraph, MongoDB Atlas, Twilio, and NPM using multi-step (lookup ID then delete) pattern.
This commit is contained in:
parent
1c3ea6cb22
commit
363b2ce77d
21 changed files with 2258 additions and 129 deletions
|
|
@ -2,6 +2,11 @@
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.79.0]
|
||||
- Added revocation support for SendGrid, Netlify, Tailscale, ElevenLabs, Sourcegraph, MongoDB Atlas, Twilio, and NPM using multi-step (lookup ID then delete) pattern.
|
||||
- Added new Sumo Logic rule with direct revocation support.
|
||||
- Added `docs/TOKEN_REVOCATION_SUPPORT.md` with detailed revocation implementation guide and testing examples.
|
||||
|
||||
## [v1.78.0]
|
||||
- Added "Skipped Validations" counter to scan summary output to distinguish between validations that failed (HTTP errors, connection failures) and validations that were skipped due to missing preconditions (e.g., missing dependent rules). This provides better visibility into validation coverage for large scans.
|
||||
- Improved error messages for `kingfisher validate` command when rules require dependent variables from `depends_on` sections. Now clearly explains which variables are needed and from which dependent rules they are normally captured.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ publish = false
|
|||
|
||||
[package]
|
||||
name = "kingfisher"
|
||||
version = "1.78.0"
|
||||
version = "1.79.0"
|
||||
description = "MongoDB's blazingly fast and accurate secret scanning and validation tool"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
|
|
|||
313
MULTI_STEP_REVOCATION_SUMMARY.md
Normal file
313
MULTI_STEP_REVOCATION_SUMMARY.md
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
# Multi-Step Revocation Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This implementation adds support for 2-step revocation processes in Kingfisher. Some API services require looking up an internal ID or metadata before the actual revocation/deletion can be performed.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Core Types (`crates/kingfisher-rules/src/rule.rs`)
|
||||
|
||||
#### New Enum Variant
|
||||
- Added `HttpMultiStep(HttpMultiStepRevocation)` to the `Revocation` enum
|
||||
|
||||
#### New Structures
|
||||
- **`HttpMultiStepRevocation`**: Contains a vector of 1-2 sequential steps
|
||||
- **`RevocationStep`**: Represents a single step with:
|
||||
- Optional step name
|
||||
- HTTP request configuration
|
||||
- Optional multipart config
|
||||
- Optional variable extraction configuration
|
||||
- **`ResponseExtractor`**: Enum for extracting values from responses:
|
||||
- `JsonPath`: Extract from JSON using JSONPath syntax
|
||||
- `Regex`: Extract using regex pattern with capture group
|
||||
- `Header`: Extract from response header
|
||||
- `Body`: Use entire response body
|
||||
- `StatusCode`: Extract HTTP status code
|
||||
|
||||
#### Restored Type
|
||||
- **`TlsMode`**: Re-added enum that was previously removed (required by validation code)
|
||||
- Added `tls_mode` field to `RuleSyntax`
|
||||
- Added `tls_mode()` method to `Rule`
|
||||
|
||||
### 2. Execution Logic (`src/direct_revoke.rs`)
|
||||
|
||||
#### New Functions
|
||||
- **`extract_value_from_response()`**: Extracts values from HTTP responses
|
||||
- Implements basic JSONPath parsing for nested objects and arrays
|
||||
- Regex extraction with first capture group
|
||||
- Header and body extraction
|
||||
- **`execute_revocation_step()`**: Executes a single revocation step
|
||||
- Renders templates with current variables
|
||||
- Performs HTTP request
|
||||
- Extracts variables from response
|
||||
- Updates globals for next step
|
||||
- **`execute_multi_step_revocation()`**: Orchestrates multi-step flow
|
||||
- Validates step count (1-2)
|
||||
- Executes steps sequentially
|
||||
- Returns result from final step
|
||||
|
||||
#### Updated Functions
|
||||
- **`extract_revocation_vars()`**: Now handles `HttpMultiStep` variant
|
||||
- **`run_direct_revocation()`**: Added match arm for `HttpMultiStep`
|
||||
|
||||
### 3. Module Exports (`crates/kingfisher-rules/src/lib.rs`)
|
||||
|
||||
Added exports for new types:
|
||||
- `HttpMultiStepRevocation`
|
||||
- `ResponseExtractor`
|
||||
- `RevocationStep`
|
||||
- `TlsMode` (restored)
|
||||
|
||||
### 4. Reporter Integration (`src/reporter.rs`)
|
||||
|
||||
Added pattern match arm for `Revocation::HttpMultiStep(_)` to generate revoke commands.
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
#### Updated `docs/RULES.md`
|
||||
- Added Section 2: "Multi-Step Revocation" with:
|
||||
- Overview and use cases
|
||||
- Response extractor types table
|
||||
- Multi-step schema documentation
|
||||
- Requirements and constraints
|
||||
- 4 comprehensive examples:
|
||||
1. Basic 2-step revocation
|
||||
2. Multiple extraction methods
|
||||
3. Complex JSONPath extraction
|
||||
4. Single-step migration path
|
||||
- Guidance on when to use multi-step vs single-step
|
||||
|
||||
#### New `docs/MULTI_STEP_REVOCATION.md`
|
||||
- Complete implementation documentation
|
||||
- Architecture details
|
||||
- API reference
|
||||
- Usage examples
|
||||
- Testing guidance
|
||||
- Error handling information
|
||||
- Debug logging instructions
|
||||
|
||||
### 6. Examples
|
||||
|
||||
Created `crates/kingfisher-rules/data/rules/example_multistep.yml` with 5 example rules:
|
||||
1. Basic 2-step with JSON extraction
|
||||
2. Multiple extractions (JSON, Header, nested)
|
||||
3. Regex extraction from XML
|
||||
4. Single-step for comparison
|
||||
5. Array extraction from JSON
|
||||
|
||||
## Features
|
||||
|
||||
### JSONPath Support
|
||||
Basic implementation supporting:
|
||||
- Nested fields: `$.data.user.id`
|
||||
- Array indexing: `$.items[0].id`
|
||||
- Combined: `$.data.sessions[0].session_id`
|
||||
|
||||
### Variable Flow
|
||||
- Variables from step 1 available in step 2
|
||||
- All standard Liquid filters work on extracted variables
|
||||
- Variables are uppercase by convention
|
||||
|
||||
### Validation
|
||||
- Minimum 1, maximum 2 steps
|
||||
- Final step requires `response_matcher`
|
||||
- Intermediate steps are optional
|
||||
- Clear error messages for all failure cases
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
All existing revocation types continue to work:
|
||||
- `Revocation::AWS` ✓
|
||||
- `Revocation::GCP` ✓
|
||||
- `Revocation::Http(_)` ✓
|
||||
- Single-step YAML format unchanged
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# Test with example rule
|
||||
kingfisher revoke --rule kingfisher.example_multistep.1 <token>
|
||||
|
||||
# With debug logging
|
||||
RUST_LOG=debug kingfisher revoke --rule <rule_id> <token>
|
||||
```
|
||||
|
||||
### Validation
|
||||
```bash
|
||||
# Compile check
|
||||
cargo check
|
||||
|
||||
# Run existing tests
|
||||
cargo test
|
||||
|
||||
# Specific revocation tests
|
||||
cargo test revoke
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
### YAML Configuration
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
- name: lookup_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.id"
|
||||
|
||||
- name: delete
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
### CLI Usage
|
||||
```bash
|
||||
# Revoke using multi-step rule
|
||||
kingfisher revoke --rule example.service <token>
|
||||
|
||||
# With additional variables
|
||||
kingfisher revoke --rule example.service --var EXTRA=value <token>
|
||||
|
||||
# JSON output
|
||||
kingfisher revoke --rule example.service --format json <token>
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Core Implementation
|
||||
- `crates/kingfisher-rules/src/rule.rs` (+115 lines)
|
||||
- `crates/kingfisher-rules/src/lib.rs` (+3 exports)
|
||||
- `src/direct_revoke.rs` (+180 lines)
|
||||
- `src/reporter.rs` (+8 lines)
|
||||
|
||||
### Documentation
|
||||
- `docs/RULES.md` (+240 lines)
|
||||
- `docs/MULTI_STEP_REVOCATION.md` (new file, 350 lines)
|
||||
- `MULTI_STEP_REVOCATION_SUMMARY.md` (this file)
|
||||
|
||||
### Examples
|
||||
- `crates/kingfisher-rules/data/rules/example_multistep.yml` (new file, 230 lines)
|
||||
|
||||
## Compilation Status
|
||||
|
||||
✅ Code compiles successfully with no warnings
|
||||
```
|
||||
cargo check
|
||||
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.51s
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
1. Support for more than 2 steps (if needed)
|
||||
2. Enhanced JSONPath implementation (more complex queries)
|
||||
3. Conditional step execution based on response values
|
||||
4. Parallel step execution where dependencies allow
|
||||
5. Step retry logic with different parameters per step
|
||||
6. Response caching between steps
|
||||
7. Variable transformation functions beyond Liquid filters
|
||||
|
||||
## Migration Guide
|
||||
|
||||
For existing single-step revocations, no changes are needed. To convert to multi-step:
|
||||
|
||||
**Before (single-step):**
|
||||
```yaml
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/tokens/revoke
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
**After (multi-step with lookup):**
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
- name: lookup_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.id"
|
||||
|
||||
- name: delete
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
- Maximum 2 steps per revocation
|
||||
- Steps execute sequentially (no parallelism)
|
||||
- Final step must have `response_matcher`
|
||||
- JSONPath implementation is basic (common patterns only)
|
||||
- Variables flow forward only (step 1 → step 2)
|
||||
|
||||
## Error Handling
|
||||
|
||||
Clear error messages for:
|
||||
- Invalid step count (< 1 or > 2)
|
||||
- Missing response_matcher on final step
|
||||
- Variable extraction failures with context
|
||||
- Invalid JSONPath expressions
|
||||
- Missing headers or response fields
|
||||
- HTTP request failures with retry logic
|
||||
|
||||
## Debug Output
|
||||
|
||||
When `RUST_LOG=debug` is set:
|
||||
- Step execution start/end
|
||||
- URLs being called (with rendered templates)
|
||||
- Variables extracted with values
|
||||
- Response status codes
|
||||
- Intermediate step completion
|
||||
- Error details with stack traces
|
||||
|
||||
## Questions & Support
|
||||
|
||||
For questions or issues:
|
||||
1. Check `docs/RULES.md` for detailed examples
|
||||
2. Review `docs/MULTI_STEP_REVOCATION.md` for implementation details
|
||||
3. Examine `crates/kingfisher-rules/data/rules/example_multistep.yml` for working examples
|
||||
4. Enable debug logging: `RUST_LOG=debug kingfisher revoke ...`
|
||||
172
REVOCATION_CHANGES_SUMMARY.md
Normal file
172
REVOCATION_CHANGES_SUMMARY.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# Revocation Support Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
Added programmatic revocation support for 9 services that provide API endpoints to delete or revoke access tokens/keys.
|
||||
|
||||
## Files Modified
|
||||
|
||||
### New Rule Created
|
||||
- **`crates/kingfisher-rules/data/rules/sumologic.yml`** - New rule for Sumo Logic Access Keys with revocation support
|
||||
|
||||
### Rules Updated with Revocation Support
|
||||
1. **`crates/kingfisher-rules/data/rules/sendgrid.yml`**
|
||||
- Added HttpMultiStep revocation (2-step: list keys → delete key)
|
||||
|
||||
2. **`crates/kingfisher-rules/data/rules/netlify.yml`**
|
||||
- Added HttpMultiStep revocation for both rule variants (2-step: list tokens → delete token)
|
||||
|
||||
3. **`crates/kingfisher-rules/data/rules/tailscale.yml`**
|
||||
- Added HttpMultiStep revocation (2-step: list keys → delete key)
|
||||
|
||||
4. **`crates/kingfisher-rules/data/rules/elevenlabs.yml`**
|
||||
- Added HttpMultiStep revocation (2-step: list API keys → delete API key)
|
||||
|
||||
5. **`crates/kingfisher-rules/data/rules/sourcegraph.yml`**
|
||||
- Added HttpMultiStep revocation for both rule variants (2-step: GraphQL query for ID → GraphQL mutation to delete)
|
||||
|
||||
6. **`crates/kingfisher-rules/data/rules/mongodb.yml`**
|
||||
- Added HttpMultiStep revocation (2-step: list groups → delete API key with Digest auth)
|
||||
|
||||
7. **`crates/kingfisher-rules/data/rules/twilio.yml`**
|
||||
- Added HttpMultiStep revocation (2-step: list accounts → delete API key)
|
||||
|
||||
8. **`crates/kingfisher-rules/data/rules/npm.yml`**
|
||||
- Added HttpMultiStep revocation for both rule variants (2-step: list tokens → revoke token)
|
||||
|
||||
### Documentation Created
|
||||
- **`docs/TOKEN_REVOCATION_SUPPORT.md`** - Comprehensive documentation of all revocation implementations
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Revocation Types Used
|
||||
|
||||
| Service | Type | Steps | Authentication | Notes |
|
||||
|---------|------|-------|----------------|-------|
|
||||
| SendGrid | HttpMultiStep | 2 | Bearer Token | Extracts first API key ID from list |
|
||||
| Netlify | HttpMultiStep | 2 | Bearer Token | Applied to both rule variants |
|
||||
| Tailscale | HttpMultiStep | 2 | Bearer Token | Lists keys from tailnet |
|
||||
| ElevenLabs | HttpMultiStep | 2 | Custom Header (xi-api-key) | Lists API keys from user account |
|
||||
| Sourcegraph | HttpMultiStep | 2 | Token Auth | Uses GraphQL queries and mutations |
|
||||
| MongoDB Atlas | HttpMultiStep | 2 | HTTP Digest | Uses public key as ID |
|
||||
| Sumo Logic | Http | 1 | Basic Auth | Direct deletion using Access ID |
|
||||
| Twilio | HttpMultiStep | 2 | Basic Auth | Requires Account SID |
|
||||
| NPM | HttpMultiStep | 2 | Bearer Token | Applied to both token formats |
|
||||
|
||||
### Multi-Step Revocation Pattern
|
||||
|
||||
All HttpMultiStep implementations follow this pattern:
|
||||
|
||||
1. **Step 1 (Lookup)**: Make a GET request to list resources and extract the relevant ID
|
||||
2. **Step 2 (Delete)**: Make a DELETE request using the extracted ID
|
||||
|
||||
### Variable Extraction
|
||||
|
||||
All implementations use **JsonPath** extraction to pull IDs from JSON responses:
|
||||
- `$.result[0].api_key_id` (SendGrid)
|
||||
- `$[0].id` (Netlify)
|
||||
- `$.keys[0].id` (Tailscale)
|
||||
- `$.api_keys[0].api_key_id` (ElevenLabs)
|
||||
- `$.data.currentUser.accessTokens.nodes[0].id` (Sourcegraph)
|
||||
- `$.results[0].id` (MongoDB - for GROUP_ID)
|
||||
- `$.accounts[0].sid` (Twilio)
|
||||
- `$.objects[0].token.key` (NPM)
|
||||
|
||||
## Services NOT Implemented
|
||||
|
||||
### Azure DevOps
|
||||
**Reason**: API returns hashed token values when listing PATs, making it impossible to safely identify which ID belongs to the current token without risking deletion of the wrong token.
|
||||
|
||||
### Azure Search
|
||||
**Reason**: Revocation requires the Management Plane (Azure ARM API), not the Data Plane API. If you only have the Search Key, you cannot revoke it via API.
|
||||
|
||||
### Sendbird
|
||||
**Reason**: Revocation requires a user ID which is not captured by current rules. Complex token types (push tokens, secondary API tokens) require additional context not available.
|
||||
|
||||
### Microsoft Teams (Graph API)
|
||||
**Reason**: Current rule is for webhooks, not Microsoft Graph API tokens. Webhook URLs cannot be revoked via API; they must be deleted from Teams admin console.
|
||||
|
||||
## Testing
|
||||
|
||||
To test revocation for any of the updated services:
|
||||
|
||||
```bash
|
||||
# Basic revocation
|
||||
kingfisher revoke --rule <rule_id> <token>
|
||||
|
||||
# With debug logging
|
||||
RUST_LOG=debug kingfisher revoke --rule <rule_id> <token>
|
||||
|
||||
# For services with dependencies (e.g., MongoDB)
|
||||
kingfisher revoke --rule kingfisher.mongodb.1 --var PUBKEY=qj4Zrh8e6A "4b18315e-6b7d-4337-b449-5d38f5a189ec"
|
||||
```
|
||||
|
||||
### Example Commands
|
||||
|
||||
```bash
|
||||
# SendGrid
|
||||
kingfisher revoke --rule kingfisher.sendgrid.1 "SG.xxx.yyy"
|
||||
|
||||
# Netlify
|
||||
kingfisher revoke --rule kingfisher.netlify.1 "3cdfad7b885a6daceff3fb820389115750b373763fb30b10ca0382648b55872d"
|
||||
|
||||
# Tailscale
|
||||
kingfisher revoke --rule kingfisher.tailscale.1 "tskey-api-xxxxx"
|
||||
|
||||
# ElevenLabs
|
||||
kingfisher revoke --rule kingfisher.elevenlabs.1 "sk_xxx"
|
||||
|
||||
# Sourcegraph
|
||||
kingfisher revoke --rule kingfisher.sourcegraph.1 "sgp_xxx"
|
||||
|
||||
# MongoDB (requires PUBKEY)
|
||||
kingfisher revoke --rule kingfisher.mongodb.1 --var PUBKEY=ABCDEFGH "4b18315e-6b7d-4337-b449-5d38f5a189ec"
|
||||
|
||||
# Sumo Logic (requires ACCESS_ID)
|
||||
kingfisher revoke --rule kingfisher.sumologic.2 --var ACCESS_ID=suXYZ123 "ABCdef123456XYZabc"
|
||||
|
||||
# Twilio (requires TWILIOID)
|
||||
kingfisher revoke --rule kingfisher.twilio.2 --var TWILIOID=SK123456 "secret_token"
|
||||
|
||||
# NPM
|
||||
kingfisher revoke --rule kingfisher.npm.1 "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"
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
- ✅ All Rust code compiles successfully (`cargo check` passes)
|
||||
- ✅ YAML syntax is valid (parsed during cargo check)
|
||||
- ✅ Follows existing multi-step revocation patterns from `example_multistep.yml`
|
||||
- ✅ Consistent with validation implementations in each service
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Token Identification Risk**: Services like SendGrid and Netlify extract the **first** token from the list. If multiple tokens exist, the wrong token might be revoked.
|
||||
|
||||
2. **Recommended Best Practices**:
|
||||
- Use these revocations only when you're certain there's a single active token
|
||||
- Consider implementing dry-run mode in the future
|
||||
- Add user prompts for confirmation before revoking
|
||||
|
||||
3. **Authentication Methods**:
|
||||
- Most services use Bearer Token authentication
|
||||
- MongoDB uses HTTP Digest authentication (properly handled)
|
||||
- Sumo Logic uses Basic Auth with Access ID and Key
|
||||
- Twilio uses Basic Auth with Account SID and API Key Secret
|
||||
|
||||
## Next Steps
|
||||
|
||||
Optional enhancements that could be added in the future:
|
||||
|
||||
1. **Interactive Mode**: Prompt user to select which token to revoke when multiple exist
|
||||
2. **Dry-Run Mode**: Show what would be revoked without actually revoking
|
||||
3. **Batch Revocation**: Revoke multiple tokens at once
|
||||
4. **Better Token Matching**: Use token metadata (name, creation date) to identify the correct token
|
||||
5. **Revocation History**: Track what was revoked and when
|
||||
|
||||
## References
|
||||
|
||||
- [Multi-Step Revocation Documentation](docs/MULTI_STEP_REVOCATION.md)
|
||||
- [Token Revocation Support Documentation](docs/TOKEN_REVOCATION_SUPPORT.md)
|
||||
- [Rules Documentation](docs/RULES.md)
|
||||
- [Example Multi-Step Rules](crates/kingfisher-rules/data/rules/example_multistep.yml)
|
||||
|
|
@ -36,3 +36,37 @@ rules:
|
|||
words:
|
||||
- '"tier"'
|
||||
- '"missing_permissions"'
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all API keys to find the current key's ID
|
||||
- name: lookup_api_key_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.elevenlabs.io/v1/user/api-keys
|
||||
headers:
|
||||
xi-api-key: "{{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first API key ID from the list
|
||||
API_KEY_ID:
|
||||
type: JsonPath
|
||||
path: "$.api_keys[0].api_key_id"
|
||||
|
||||
# Step 2: Delete the API key using its ID
|
||||
- name: delete_api_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.elevenlabs.io/v1/user/api-keys/{{ API_KEY_ID }}
|
||||
headers:
|
||||
xi-api-key: "{{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
|
|
|
|||
240
crates/kingfisher-rules/data/rules/example_multistep.yml
Normal file
240
crates/kingfisher-rules/data/rules/example_multistep.yml
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Example rules demonstrating multi-step revocation
|
||||
#
|
||||
# This file shows how to configure 2-step revocation processes for services
|
||||
# that require looking up an ID before performing the actual revocation.
|
||||
|
||||
rules:
|
||||
# Example 1: Basic 2-step revocation with JSON extraction
|
||||
- name: Example API Token (2-step revocation)
|
||||
id: kingfisher.example_multistep.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
example_api_token_
|
||||
[A-Za-z0-9]{40}
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- "EXAMPLE_TOKEN=example_api_token_abc123def456ghi789jkl012mno345pqrs"
|
||||
references:
|
||||
- https://example.com/docs/api-tokens
|
||||
|
||||
# Standard single-step validation
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/auth/verify
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
# Multi-step revocation: lookup ID first, then delete
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get the token's internal ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the token ID from JSON response
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.token_id"
|
||||
|
||||
# Step 2: Delete the token using its ID
|
||||
- name: delete_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/v1/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204, 200]
|
||||
|
||||
# Example 2: Multi-step with multiple extractions
|
||||
- name: Complex Service Token (multi-extraction)
|
||||
id: kingfisher.example_multistep.2
|
||||
pattern: |
|
||||
(?xi)
|
||||
complex_token_
|
||||
[A-Za-z0-9_-]{32,}
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- "TOKEN=complex_token_xyz789_abc123_def456_ghi789"
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get multiple pieces of information
|
||||
- name: get_token_metadata
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.complex.com/v2/tokens/info
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
# Extract from JSON body using JSONPath
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.id"
|
||||
|
||||
# Extract from response header
|
||||
ACCOUNT_ID:
|
||||
type: Header
|
||||
name: X-Account-ID
|
||||
|
||||
# Extract nested JSON field
|
||||
WORKSPACE_ID:
|
||||
type: JsonPath
|
||||
path: "$.workspace.id"
|
||||
|
||||
# Step 2: Use all extracted values in revocation request
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.complex.com/v2/accounts/{{ ACCOUNT_ID }}/workspaces/{{ WORKSPACE_ID }}/tokens/{{ TOKEN_ID }}/revoke
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: '{"reason":"Token compromised","force":true}'
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
- type: WordMatch
|
||||
words: ['"success":true']
|
||||
|
||||
# Example 3: Using regex extraction
|
||||
- name: Service With XML Response
|
||||
id: kingfisher.example_multistep.3
|
||||
pattern: |
|
||||
(?xi)
|
||||
xml_service_key_
|
||||
[A-Fa-f0-9]{32}
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- "KEY=xml_service_key_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Parse XML response with regex
|
||||
- name: get_key_id_from_xml
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.xmlservice.com/keys/current
|
||||
headers:
|
||||
X-API-Key: "{{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: XmlValid
|
||||
extract:
|
||||
# Use regex to extract from XML
|
||||
KEY_ID:
|
||||
type: Regex
|
||||
pattern: '<KeyId>([^<]+)</KeyId>'
|
||||
|
||||
# Step 2: Delete using extracted ID
|
||||
- name: delete_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.xmlservice.com/keys/{{ KEY_ID }}
|
||||
headers:
|
||||
X-API-Key: "{{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
|
||||
# Example 4: Single-step (for comparison)
|
||||
# This shows that simple revocations don't need the multi-step approach
|
||||
- name: Simple Service Token (single-step)
|
||||
id: kingfisher.example_multistep.4
|
||||
pattern: |
|
||||
(?xi)
|
||||
simple_token_
|
||||
[A-Za-z0-9]{32}
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- "TOKEN=simple_token_abcdefghijklmnopqrstuvwxyz123456"
|
||||
|
||||
# This service accepts the token directly for revocation
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.simple.com/v1/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
||||
# Example 5: Array extraction from JSON
|
||||
- name: Service With Array Response
|
||||
id: kingfisher.example_multistep.5
|
||||
pattern: |
|
||||
(?xi)
|
||||
array_service_
|
||||
[A-Za-z0-9]{28}
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- "TOKEN=array_service_abcdefghijklmnopqrstuvwxyz12"
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get the first session ID from an array
|
||||
- name: get_session_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.arrayservice.com/sessions
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract first element from array
|
||||
SESSION_ID:
|
||||
type: JsonPath
|
||||
path: "$.sessions[0].id"
|
||||
|
||||
# Step 2: Terminate the session
|
||||
- name: terminate_session
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.arrayservice.com/sessions/{{ SESSION_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
|
@ -51,6 +51,42 @@ rules:
|
|||
depends_on_rule:
|
||||
- rule_id: "kingfisher.mongodb.2"
|
||||
variable: PUBKEY
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get the first GROUP_ID (Project ID)
|
||||
- name: lookup_group_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://cloud.mongodb.com/api/atlas/v2/groups
|
||||
headers:
|
||||
Accept: application/vnd.atlas.2023-02-01+json
|
||||
Content-Type: application/json
|
||||
digest: "{{ PUBKEY | append: ':' | append: TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first group/project ID
|
||||
GROUP_ID:
|
||||
type: JsonPath
|
||||
path: "$.results[0].id"
|
||||
|
||||
# Step 2: Delete the API key using the public key as ID
|
||||
- name: delete_api_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://cloud.mongodb.com/api/atlas/v2/groups/{{ GROUP_ID }}/apiKeys/{{ PUBKEY }}
|
||||
headers:
|
||||
Accept: application/vnd.atlas.2023-02-01+json
|
||||
digest: "{{ PUBKEY | append: ':' | append: TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
||||
- name: MongoDB API PUBLIC Key
|
||||
id: kingfisher.mongodb.2
|
||||
|
|
|
|||
|
|
@ -30,6 +30,40 @@ rules:
|
|||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all access tokens to find the current token's ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.netlify.com/api/v1/access_tokens
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first token ID from the list
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$[0].id"
|
||||
|
||||
# Step 2: Delete the access token using its ID
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.netlify.com/api/v1/access_tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
||||
- name: Netlify API Key
|
||||
id: kingfisher.netlify.2
|
||||
|
|
@ -64,3 +98,37 @@ rules:
|
|||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all access tokens to find the current token's ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.netlify.com/api/v1/access_tokens
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first token ID from the list
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$[0].id"
|
||||
|
||||
# Step 2: Delete the access token using its ID
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.netlify.com/api/v1/access_tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,41 @@ rules:
|
|||
status: [200]
|
||||
- type: WordMatch
|
||||
words: ['"name":']
|
||||
url: https://registry.npmjs.org/-/npm/v1/user
|
||||
url: https://registry.npmjs.org/-/npm/v1/user
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all tokens to find the current token's key ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://registry.npmjs.org/-/npm/v1/tokens
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first token's key
|
||||
TOKEN_KEY:
|
||||
type: JsonPath
|
||||
path: "$.objects[0].token.key"
|
||||
|
||||
# Step 2: Revoke the token using its key
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://registry.npmjs.org/-/npm/v1/tokens/token/{{ TOKEN_KEY }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
|
||||
- name: NPM Access Token (old format)
|
||||
id: kingfisher.npm.2
|
||||
|
|
@ -69,4 +103,38 @@ rules:
|
|||
status: [200]
|
||||
- type: WordMatch
|
||||
words: ['"name":']
|
||||
url: https://registry.npmjs.org/-/npm/v1/user
|
||||
url: https://registry.npmjs.org/-/npm/v1/user
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all tokens to find the current token's key ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://registry.npmjs.org/-/npm/v1/tokens
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first token's key
|
||||
TOKEN_KEY:
|
||||
type: JsonPath
|
||||
path: "$.objects[0].token.key"
|
||||
|
||||
# Step 2: Revoke the token using its key
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://registry.npmjs.org/-/npm/v1/tokens/token/{{ TOKEN_KEY }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
|
|
@ -37,4 +37,40 @@ rules:
|
|||
status: [200]
|
||||
- type: WordMatch
|
||||
match_all_words: true
|
||||
words: ['"reputation"', '"type"']
|
||||
words: ['"reputation"', '"type"']
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all API keys to find the current key's ID
|
||||
- name: lookup_api_key_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.sendgrid.com/v3/api_keys
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first API key ID from the list
|
||||
# Note: SendGrid only shows partial keys, so we extract the first one
|
||||
# which should be the current token if there's only one active key
|
||||
API_KEY_ID:
|
||||
type: JsonPath
|
||||
path: "$.result[0].api_key_id"
|
||||
|
||||
# Step 2: Delete the API key using its ID
|
||||
- name: delete_api_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.sendgrid.com/v3/api_keys/{{ API_KEY_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
|
@ -2,10 +2,10 @@ rules:
|
|||
- name: Sourcegraph Access Token
|
||||
id: kingfisher.sourcegraph.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
sgp_(?:[A-F0-9]{16}|local)_[A-F0-9]{40}|sgp_[A-F0-9]{40}
|
||||
sgp_(?:[a-fA-F0-9]{16}|local)_[a-fA-F0-9]{40}|sgp_[a-fA-F0-9]{40}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
|
|
@ -31,6 +31,44 @@ rules:
|
|||
- type: WordMatch
|
||||
words: ['"site":{']
|
||||
match_all_words: true
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Query to get the current token's ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: POST
|
||||
url: https://sourcegraph.com/.api/graphql
|
||||
headers:
|
||||
Authorization: "token {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: |
|
||||
{ "query": "query GetCurrentToken { currentUser { accessTokens(first: 1) { nodes { id } } } }" }
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.currentUser.accessTokens.nodes[0].id"
|
||||
|
||||
# Step 2: Delete the access token via GraphQL mutation
|
||||
- name: delete_token
|
||||
request:
|
||||
method: POST
|
||||
url: https://sourcegraph.com/.api/graphql
|
||||
headers:
|
||||
Authorization: "token {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: |
|
||||
{ "query": "mutation DeleteToken($id: ID!) { deleteAccessToken(byID: $id) { alwaysNil } }", "variables": { "id": "{{ TOKEN_ID }}" } }
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
- name: Sourcegraph _Legacy_ API Key
|
||||
id: kingfisher.sourcegraph.2
|
||||
|
|
@ -68,6 +106,44 @@ rules:
|
|||
status: [200]
|
||||
- type: WordMatch
|
||||
words: ['"site":{']
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Query to get the current token's ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: POST
|
||||
url: https://sourcegraph.com/.api/graphql
|
||||
headers:
|
||||
Authorization: "token {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: |
|
||||
{ "query": "query GetCurrentToken { currentUser { accessTokens(first: 1) { nodes { id } } } }" }
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.currentUser.accessTokens.nodes[0].id"
|
||||
|
||||
# Step 2: Delete the access token via GraphQL mutation
|
||||
- name: delete_token
|
||||
request:
|
||||
method: POST
|
||||
url: https://sourcegraph.com/.api/graphql
|
||||
headers:
|
||||
Authorization: "token {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: |
|
||||
{ "query": "mutation DeleteToken($id: ID!) { deleteAccessToken(byID: $id) { alwaysNil } }", "variables": { "id": "{{ TOKEN_ID }}" } }
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
- name: Sourcegraph Cody Gateway Key
|
||||
id: kingfisher.sourcegraph.3
|
||||
|
|
|
|||
80
crates/kingfisher-rules/data/rules/sumologic.yml
Normal file
80
crates/kingfisher-rules/data/rules/sumologic.yml
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
rules:
|
||||
- name: Sumo Logic Access ID
|
||||
id: kingfisher.sumologic.1
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
sumo
|
||||
(?:.|[\n\r]){0,32}?
|
||||
(?:access|id)
|
||||
(?:.|[\n\r]){0,16}?
|
||||
\b
|
||||
(
|
||||
su[A-Za-z0-9]{10,14}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 2
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
visible: false
|
||||
examples:
|
||||
- sumo_access_id=suABCDEF1234567890XYZABC
|
||||
- 'SUMO_ACCESS_ID: suXYZ123456ABC789DEF012'
|
||||
|
||||
- name: Sumo Logic Access Key
|
||||
id: kingfisher.sumologic.2
|
||||
pattern: |
|
||||
(?xi)
|
||||
\b
|
||||
sumo
|
||||
(?:.|[\n\r]){0,32}?
|
||||
(?:SECRET|PRIVATE|ACCESS|KEY|TOKEN)
|
||||
(?:.|[\n\r]){0,32}?
|
||||
\b
|
||||
(
|
||||
[A-Za-z0-9]{62,64}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 2
|
||||
min_uppercase: 2
|
||||
min_lowercase: 2
|
||||
min_entropy: 3.5
|
||||
confidence: medium
|
||||
examples:
|
||||
- sumo_access_key=ABCdef123456XYZabc789012DEFghi345678PQRstu
|
||||
- 'SUMO_ACCESS_KEY: XYZ123abc456DEF789ghi012JKL345mno678PQR901stu'
|
||||
references:
|
||||
- https://help.sumologic.com/docs/manage/security/access-keys/
|
||||
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.sumologic.com/api/v1/accessKeys
|
||||
headers:
|
||||
Accept: application/json
|
||||
Authorization: "Basic {{ ACCESS_ID | append: ':' | append: TOKEN | b64enc }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
depends_on_rule:
|
||||
- rule_id: "kingfisher.sumologic.1"
|
||||
variable: ACCESS_ID
|
||||
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.sumologic.com/api/v1/accessKeys/{{ ACCESS_ID }}
|
||||
headers:
|
||||
Authorization: "Basic {{ ACCESS_ID | append: ':' | append: TOKEN | b64enc }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
|
@ -30,3 +30,37 @@ rules:
|
|||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: List all keys to find the current key's ID
|
||||
- name: lookup_key_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.tailscale.com/api/v2/tailnet/-/keys
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Accept: application/json
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first key ID from the list
|
||||
KEY_ID:
|
||||
type: JsonPath
|
||||
path: "$.keys[0].id"
|
||||
|
||||
# Step 2: Delete the key using its ID
|
||||
- name: delete_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.tailscale.com/api/v2/key/{{ KEY_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
|
|
|
|||
|
|
@ -60,4 +60,40 @@ rules:
|
|||
url: https://api.twilio.com/2010-04-01/Accounts.json
|
||||
depends_on_rule:
|
||||
- rule_id: "kingfisher.twilio.1"
|
||||
variable: TWILIOID
|
||||
variable: TWILIOID
|
||||
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get the Account SID (needed if TWILIOID is an API Key starting with SK)
|
||||
- name: lookup_account_sid
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.twilio.com/2010-04-01/Accounts.json
|
||||
headers:
|
||||
Accept: application/json
|
||||
Authorization: "Basic {{ TWILIOID | append: ':' | append: TOKEN | b64enc }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
# Extract the first account SID
|
||||
ACCOUNT_SID:
|
||||
type: JsonPath
|
||||
path: "$.accounts[0].sid"
|
||||
|
||||
# Step 2: Delete the API key using the TWILIOID (should be SK...) and ACCOUNT_SID
|
||||
# Note: This assumes TWILIOID is an API Key SID (SK...). If it's an Account SID (AC...),
|
||||
# this will fail, but you typically don't delete account SIDs.
|
||||
- name: delete_api_key
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.twilio.com/2010-04-01/Accounts/{{ ACCOUNT_SID }}/Keys/{{ TWILIOID }}.json
|
||||
headers:
|
||||
Authorization: "Basic {{ ACCOUNT_SID | append: ':' | append: TOKEN | b64enc }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
|
|
@ -16,10 +16,11 @@ pub mod rules_database;
|
|||
|
||||
// Re-export rule types
|
||||
pub use rule::{
|
||||
ChecksumActual, ChecksumRequirement, Confidence, DependsOnRule, HttpRequest, HttpValidation,
|
||||
MultipartConfig, MultipartPart, PatternRequirementContext, PatternRequirements,
|
||||
PatternValidationResult, ReportResponseData, ResponseMatcher, Revocation, Rule, RuleSyntax,
|
||||
TlsMode, Validation, RULE_COMMENTS_PATTERN,
|
||||
ChecksumActual, ChecksumRequirement, Confidence, DependsOnRule, HttpMultiStepRevocation,
|
||||
HttpRequest, HttpValidation, MultipartConfig, MultipartPart, PatternRequirementContext,
|
||||
PatternRequirements, PatternValidationResult, ReportResponseData, ResponseExtractor,
|
||||
ResponseMatcher, Revocation, RevocationStep, Rule, RuleSyntax, TlsMode, Validation,
|
||||
RULE_COMMENTS_PATTERN,
|
||||
};
|
||||
|
||||
// Re-export Rules collection
|
||||
|
|
|
|||
|
|
@ -88,6 +88,61 @@ pub enum Revocation {
|
|||
AWS,
|
||||
GCP,
|
||||
Http(HttpValidation),
|
||||
/// Multi-step HTTP revocation (up to 2 steps).
|
||||
/// Some services require looking up an ID before deletion.
|
||||
HttpMultiStep(HttpMultiStepRevocation),
|
||||
}
|
||||
|
||||
/// Configuration for multi-step HTTP revocation.
|
||||
///
|
||||
/// This allows up to 2 steps where the first step can extract values
|
||||
/// (e.g., an ID) from its response, which are then used in the second step.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct HttpMultiStepRevocation {
|
||||
/// Sequential steps to execute (minimum 1, maximum 2).
|
||||
pub steps: Vec<RevocationStep>,
|
||||
}
|
||||
|
||||
/// A single step in a multi-step revocation process.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
pub struct RevocationStep {
|
||||
/// Human-readable name for this step (e.g., "lookup_id", "delete").
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// HTTP request configuration for this step.
|
||||
pub request: HttpRequest,
|
||||
|
||||
/// Optional multipart configuration for this step.
|
||||
#[serde(default)]
|
||||
pub multipart: Option<MultipartConfig>,
|
||||
|
||||
/// Variables to extract from the response for use in subsequent steps.
|
||||
/// Keys are variable names (uppercase), values are extraction patterns.
|
||||
#[serde(default)]
|
||||
pub extract: Option<BTreeMap<String, ResponseExtractor>>,
|
||||
}
|
||||
|
||||
/// Describes how to extract a value from an HTTP response.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ResponseExtractor {
|
||||
/// Extract from JSON response using a JSONPath-like syntax.
|
||||
/// Example: "$.data.id" or "$.items[0].token_id"
|
||||
JsonPath { path: String },
|
||||
|
||||
/// Extract using a regex pattern with a capture group.
|
||||
/// The first capture group is used as the extracted value.
|
||||
Regex { pattern: String },
|
||||
|
||||
/// Extract an HTTP response header value.
|
||||
Header { name: String },
|
||||
|
||||
/// Extract the entire response body as-is.
|
||||
Body,
|
||||
|
||||
/// Extract the HTTP status code as a string.
|
||||
StatusCode,
|
||||
}
|
||||
|
||||
/// Specifies that a rule depends on a variable from another rule.
|
||||
|
|
@ -561,14 +616,7 @@ 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.
|
||||
/// Optional TLS mode for validation connections.
|
||||
#[serde(default)]
|
||||
pub tls_mode: Option<TlsMode>,
|
||||
}
|
||||
|
|
@ -749,11 +797,7 @@ impl Rule {
|
|||
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.
|
||||
/// Returns the TLS mode for this rule, if specified.
|
||||
pub fn tls_mode(&self) -> Option<TlsMode> {
|
||||
self.syntax.tls_mode
|
||||
}
|
||||
|
|
@ -1053,104 +1097,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
257
docs/MULTI_STEP_REVOCATION.md
Normal file
257
docs/MULTI_STEP_REVOCATION.md
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
# Multi-Step Revocation Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the implementation of 2-step revocation support in Kingfisher. Some services require a two-step revocation process:
|
||||
|
||||
1. **Step 1 (Lookup)**: Query the API to retrieve an internal ID, token identifier, or other metadata
|
||||
2. **Step 2 (Delete)**: Use the extracted value(s) to perform the actual revocation/deletion
|
||||
|
||||
## Architecture
|
||||
|
||||
### New Types
|
||||
|
||||
#### `HttpMultiStepRevocation`
|
||||
```rust
|
||||
pub struct HttpMultiStepRevocation {
|
||||
/// Sequential steps to execute (minimum 1, maximum 2).
|
||||
pub steps: Vec<RevocationStep>,
|
||||
}
|
||||
```
|
||||
|
||||
#### `RevocationStep`
|
||||
```rust
|
||||
pub struct RevocationStep {
|
||||
/// Human-readable name for this step (e.g., "lookup_id", "delete").
|
||||
pub name: Option<String>,
|
||||
|
||||
/// HTTP request configuration for this step.
|
||||
pub request: HttpRequest,
|
||||
|
||||
/// Optional multipart configuration for this step.
|
||||
pub multipart: Option<MultipartConfig>,
|
||||
|
||||
/// Variables to extract from the response for use in subsequent steps.
|
||||
pub extract: Option<BTreeMap<String, ResponseExtractor>>,
|
||||
}
|
||||
```
|
||||
|
||||
#### `ResponseExtractor`
|
||||
```rust
|
||||
pub enum ResponseExtractor {
|
||||
/// Extract from JSON response using JSONPath syntax
|
||||
JsonPath { path: String },
|
||||
|
||||
/// Extract using regex with a capture group
|
||||
Regex { pattern: String },
|
||||
|
||||
/// Extract an HTTP response header value
|
||||
Header { name: String },
|
||||
|
||||
/// Use the entire response body as-is
|
||||
Body,
|
||||
|
||||
/// Extract the HTTP status code as a string
|
||||
StatusCode,
|
||||
}
|
||||
```
|
||||
|
||||
### Revocation Enum
|
||||
|
||||
The `Revocation` enum has been extended with:
|
||||
|
||||
```rust
|
||||
pub enum Revocation {
|
||||
AWS,
|
||||
GCP,
|
||||
Http(HttpValidation),
|
||||
HttpMultiStep(HttpMultiStepRevocation), // New variant
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Execution Flow
|
||||
|
||||
1. **Validation**: Checks that 1-2 steps are defined
|
||||
2. **Sequential Execution**: Each step executes in order
|
||||
3. **Variable Extraction**: After each step completes, extract variables from response
|
||||
4. **Variable Injection**: Extracted variables are available as Liquid templates in subsequent steps
|
||||
5. **Response Validation**: Final step's `response_matcher` determines success/failure
|
||||
|
||||
### Key Functions
|
||||
|
||||
#### `extract_value_from_response()`
|
||||
Extracts a value from an HTTP response based on the specified extractor type.
|
||||
|
||||
**Supported Extractors:**
|
||||
- **JsonPath**: Basic JSONPath implementation supporting:
|
||||
- Nested fields: `$.data.user.id`
|
||||
- Array indexing: `$.items[0].id`
|
||||
- Combined: `$.data.sessions[0].session_id`
|
||||
- **Regex**: Uses first capture group from pattern match
|
||||
- **Header**: Extracts value from response header by name
|
||||
- **Body**: Returns entire response body
|
||||
- **StatusCode**: Returns HTTP status code as string
|
||||
|
||||
#### `execute_revocation_step()`
|
||||
Executes a single revocation step:
|
||||
1. Renders URL and request templates with current variables
|
||||
2. Builds and sends HTTP request
|
||||
3. Extracts variables from response if configured
|
||||
4. Adds extracted variables to globals for next step
|
||||
|
||||
#### `execute_multi_step_revocation()`
|
||||
Orchestrates the multi-step revocation process:
|
||||
1. Validates step count (1-2 steps)
|
||||
2. Iterates through steps sequentially
|
||||
3. Tracks intermediate results
|
||||
4. Returns final result from last step
|
||||
|
||||
### Backwards Compatibility
|
||||
|
||||
All existing single-step revocations continue to work unchanged:
|
||||
- `Revocation::AWS`
|
||||
- `Revocation::GCP`
|
||||
- `Revocation::Http(_)`
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic 2-Step Revocation
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get the token ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.token_id"
|
||||
|
||||
# Step 2: Delete the token
|
||||
- name: delete_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/v1/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
### Multiple Extractions
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
- name: get_metadata
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.service.com/tokens/info
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.id"
|
||||
ACCOUNT_ID:
|
||||
type: Header
|
||||
name: X-Account-ID
|
||||
TOKEN_TYPE:
|
||||
type: Regex
|
||||
pattern: '"type":\s*"([^"]+)"'
|
||||
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.service.com/accounts/{{ ACCOUNT_ID }}/tokens/{{ TOKEN_ID }}/revoke
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: '{"token_type":"{{ TOKEN_TYPE }}"}'
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Test your multi-step revocation using:
|
||||
|
||||
```bash
|
||||
# Revoke a token using multi-step revocation
|
||||
kingfisher revoke --rule <rule_id> <token>
|
||||
|
||||
# With additional variables if needed
|
||||
kingfisher revoke --rule <rule_id> --var EXTRA_VAR=value <token>
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Core Implementation
|
||||
- `crates/kingfisher-rules/src/rule.rs`: Added new types and enum variants
|
||||
- `crates/kingfisher-rules/src/lib.rs`: Exported new types
|
||||
- `src/direct_revoke.rs`: Added multi-step execution logic
|
||||
|
||||
### Documentation
|
||||
- `docs/RULES.md`: Added comprehensive multi-step revocation documentation
|
||||
- `docs/MULTI_STEP_REVOCATION.md`: This file
|
||||
|
||||
### Examples
|
||||
- `crates/kingfisher-rules/data/rules/example_multistep.yml`: Example rules demonstrating multi-step revocation
|
||||
|
||||
### Supporting Changes
|
||||
- `src/reporter.rs`: Added pattern match for `HttpMultiStep` variant
|
||||
|
||||
## Constraints
|
||||
|
||||
1. **Maximum 2 steps**: The implementation supports 1-2 steps only
|
||||
2. **Sequential execution**: Steps execute in order; no parallel execution
|
||||
3. **Final step validation**: The last step must include `response_matcher`
|
||||
4. **Variable naming**: Extracted variable names should be uppercase (convention)
|
||||
5. **JSONPath limitations**: Basic implementation supporting common patterns only
|
||||
|
||||
## Error Handling
|
||||
|
||||
The implementation provides clear error messages for:
|
||||
- Empty steps array
|
||||
- More than 2 steps
|
||||
- Missing response_matcher on final step
|
||||
- Failed variable extraction
|
||||
- Invalid JSONPath syntax
|
||||
- Missing required headers or fields
|
||||
- HTTP request failures
|
||||
|
||||
All errors are propagated with context about which step failed and why.
|
||||
|
||||
## Debug Logging
|
||||
|
||||
Enable debug logging to see multi-step execution details:
|
||||
|
||||
```bash
|
||||
RUST_LOG=debug kingfisher revoke --rule <rule_id> <token>
|
||||
```
|
||||
|
||||
Debug logs include:
|
||||
- Step execution start/completion
|
||||
- URLs being called
|
||||
- Variables extracted and their values
|
||||
- Response status codes
|
||||
- Intermediate step results
|
||||
271
docs/RULES.md
271
docs/RULES.md
|
|
@ -100,6 +100,45 @@ revocation:
|
|||
type: GCP
|
||||
```
|
||||
|
||||
### Multi-Step Revocation
|
||||
|
||||
Some services require a 2-step revocation process:
|
||||
1. **Lookup Step**: Make a request to retrieve an ID or token
|
||||
2. **Delete Step**: Use that ID to perform the actual revocation
|
||||
|
||||
For these cases, use `HttpMultiStep`:
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
- name: lookup_token_id # Step 1: Get the token ID
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract: # Extract values from response
|
||||
TOKEN_ID: # Variable name (uppercase)
|
||||
type: JsonPath # Extraction method
|
||||
path: "$.data.id" # JSONPath to the value
|
||||
|
||||
- name: revoke_token # Step 2: Delete using the ID
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/v1/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
| Field | What it does |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| name | Friendly name shown in reports |
|
||||
|
|
@ -112,7 +151,7 @@ revocation:
|
|||
| depends_on_rule | Chain rules: use captures from one rule in another's validation |
|
||||
| pattern_requirements | Require character types and/or exclude placeholder words from matches |
|
||||
| validation | Configure HTTP, AWS, GCP, etc. checks to verify live validity |
|
||||
| revocation | Configure HTTP or AWS revocation actions for a detected secret |
|
||||
| revocation | Configure HTTP, AWS, or multi-step revocation for a detected secret |
|
||||
|
||||
|
||||
*responser_matcher* variants. Multiple can be used
|
||||
|
|
@ -125,10 +164,234 @@ revocation:
|
|||
| **XmlValid** | – | Pass only if body parses as well-formed XML. Use when response is expected as XML data |
|
||||
| **ReportResponse** | `report_response` (bool) | Include raw payload in finding for debugging. |
|
||||
|
||||
## 2. Templating with Liquid
|
||||
## 2. Multi-Step Revocation
|
||||
|
||||
Some APIs require a two-step revocation process:
|
||||
|
||||
1. **Step 1 (Lookup)**: Query the API to retrieve an internal ID, token identifier, or other metadata
|
||||
2. **Step 2 (Delete)**: Use the extracted value(s) to perform the actual revocation/deletion
|
||||
|
||||
Kingfisher supports up to 2 sequential steps in a revocation workflow. Each step can extract values from its response, making them available as variables in subsequent steps.
|
||||
|
||||
### Response Extractors
|
||||
|
||||
Values can be extracted from HTTP responses using the following methods:
|
||||
|
||||
| Extractor Type | Description | Example |
|
||||
|----------------|-------------|---------|
|
||||
| **JsonPath** | Extract from JSON response using JSONPath syntax | `$.data.id`, `$.items[0].token_id` |
|
||||
| **Regex** | Extract using regex with a capture group | `"token_id":\s*"([^"]+)"` |
|
||||
| **Header** | Extract an HTTP response header value | `X-Token-ID` |
|
||||
| **Body** | Use the entire response body as-is | - |
|
||||
| **StatusCode** | Extract the HTTP status code as a string | - |
|
||||
|
||||
### Multi-Step Revocation Schema
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
- name: <step_name> # Optional: human-readable step name
|
||||
request: # Standard HTTP request configuration
|
||||
method: GET|POST|DELETE|...
|
||||
url: https://api.example.com/...
|
||||
headers:
|
||||
Header-Name: "value"
|
||||
body: "optional request body"
|
||||
response_matcher: # Required for final step only
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract: # Optional: extract variables from response
|
||||
VARIABLE_NAME: # Variable name (uppercase recommended)
|
||||
type: JsonPath|Regex|Header|Body|StatusCode
|
||||
path: "$.path.to.value" # For JsonPath
|
||||
pattern: "regex pattern" # For Regex (use first capture group)
|
||||
name: "header-name" # For Header
|
||||
|
||||
- name: <next_step> # Subsequent steps can use extracted variables
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/tokens/{{ VARIABLE_NAME }}
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
### Multi-Step Revocation Requirements
|
||||
|
||||
- **Minimum 1, Maximum 2 steps**: You must define at least 1 step and no more than 2 steps
|
||||
- **Final step requires response_matcher**: The last step must include a `response_matcher` to determine success/failure
|
||||
- **Intermediate steps are optional**: Earlier steps don't require response matchers but can have them for validation
|
||||
- **Variables flow forward**: Variables extracted in step 1 are available in step 2 via Liquid templates (e.g., `{{ TOKEN_ID }}`)
|
||||
- **All standard Liquid filters apply**: You can use filters on extracted variables just like with `{{ TOKEN }}`
|
||||
|
||||
### Example 1: Basic Two-Step Revocation
|
||||
|
||||
This example shows a service that requires looking up a token's ID before deletion:
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- name: Example Service Token
|
||||
id: kingfisher.example.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
example_token_
|
||||
[A-Za-z0-9]{32}
|
||||
min_entropy: 3.5
|
||||
examples:
|
||||
- example_token_abc123def456ghi789jkl012mno345
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/auth/verify
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Look up the token ID
|
||||
- name: lookup_token_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.example.com/v1/tokens/current
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.token_id"
|
||||
|
||||
# Step 2: Delete the token using the ID
|
||||
- name: delete_token
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.example.com/v1/tokens/{{ TOKEN_ID }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
### Example 2: Using Multiple Extraction Methods
|
||||
|
||||
This example demonstrates extracting values using different methods:
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Get metadata from multiple sources
|
||||
- name: get_token_metadata
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.service.com/tokens/info
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
extract:
|
||||
# Extract from JSON body
|
||||
TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.id"
|
||||
|
||||
# Extract from response header
|
||||
ACCOUNT_ID:
|
||||
type: Header
|
||||
name: X-Account-ID
|
||||
|
||||
# Extract using regex
|
||||
TOKEN_TYPE:
|
||||
type: Regex
|
||||
pattern: '"type":\s*"([^"]+)"'
|
||||
|
||||
# Step 2: Use all extracted values
|
||||
- name: revoke_token
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.service.com/accounts/{{ ACCOUNT_ID }}/tokens/{{ TOKEN_ID }}/revoke
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
Content-Type: application/json
|
||||
body: '{"token_type":"{{ TOKEN_TYPE }}"}'
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
```
|
||||
|
||||
### Example 3: Complex JSONPath Extraction
|
||||
|
||||
JSONPath supports nested objects and array indexing:
|
||||
|
||||
```yaml
|
||||
extract:
|
||||
# Extract from nested object
|
||||
USER_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.user.id"
|
||||
|
||||
# Extract from array (first element)
|
||||
FIRST_TOKEN_ID:
|
||||
type: JsonPath
|
||||
path: "$.tokens[0].id"
|
||||
|
||||
# Extract from nested array
|
||||
SESSION_ID:
|
||||
type: JsonPath
|
||||
path: "$.data.sessions[0].session_id"
|
||||
```
|
||||
|
||||
### Example 4: Single-Step Migration Path
|
||||
|
||||
Existing single-step revocations remain unchanged and continue to work:
|
||||
|
||||
```yaml
|
||||
# This continues to work as before
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.service.com/tokens/revoke
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [204]
|
||||
```
|
||||
|
||||
### When to Use Multi-Step Revocation
|
||||
|
||||
Use multi-step revocation when:
|
||||
|
||||
- **The API requires looking up an ID first**: Some services don't accept the token directly for revocation
|
||||
- **You need metadata from the token**: The revocation endpoint requires additional information only available via a separate API call
|
||||
- **The service uses indirect revocation**: The token must be associated with another resource (session, key, credential) that needs to be identified first
|
||||
|
||||
Do NOT use multi-step revocation when:
|
||||
|
||||
- **The API accepts the token directly**: Use the simpler single-step `Http` revocation
|
||||
- **You need more than 2 steps**: Kingfisher supports a maximum of 2 steps
|
||||
- **The service provides a native revocation method**: Use `AWS` or `GCP` types when applicable
|
||||
|
||||
## 3. Templating with Liquid
|
||||
Kingfisher leverages the Liquid template engine for dynamic parts of HTTP request bodies, headers, query parameters, and multipart payloads. The engine supports both built-in and custom filters to manipulate the captured secret (TOKEN) or other named captures ({{ NAME }}).
|
||||
|
||||
### Using Liquid Filters in Validation
|
||||
### Using Liquid Filters in Validation and Revocation
|
||||
- **Capture Injection**: The unnamed capture from your regex becomes {{ TOKEN }}. Named captures are made available as uppercase variables (e.g. {{ RDMVAL }}).
|
||||
- **Filter Pipeline**: You can chain filters using the pipe (|) syntax:
|
||||
|
||||
|
|
@ -141,7 +404,7 @@ Arguments: Some filters accept parameters, provided after a colon:
|
|||
{{ TOKEN | hmac_sha256: "my-secret-key" }}
|
||||
```
|
||||
|
||||
### 3. Built-in & Custom Liquid Filters
|
||||
### Built-in & Custom Liquid Filters
|
||||
|
||||
Below is the complete list of Liquid filters available in Kingfisher, along with their usage patterns and examples.
|
||||
| Filter | Parameters | Description | Example |
|
||||
|
|
|
|||
232
docs/TOKEN_REVOCATION_SUPPORT.md
Normal file
232
docs/TOKEN_REVOCATION_SUPPORT.md
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
# Token Revocation Support
|
||||
|
||||
This document provides an overview of the revocation support added to Kingfisher for various service tokens.
|
||||
|
||||
## Overview
|
||||
|
||||
Revocation support has been added for services that provide programmatic API endpoints to delete or revoke access tokens/keys. Most implementations use the **HttpMultiStep** revocation type because they require a two-step process:
|
||||
|
||||
1. **Step 1 (Lookup)**: Query the API to retrieve an internal ID or token identifier
|
||||
2. **Step 2 (Delete)**: Use the extracted ID to perform the actual revocation
|
||||
|
||||
## Services with Revocation Support
|
||||
|
||||
### 1. SendGrid (`sendgrid.yml`)
|
||||
- **Rule ID**: `kingfisher.sendgrid.1`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /v3/api_keys/{api_key_id}`
|
||||
- **Process**:
|
||||
1. List all API keys to find the current key's ID
|
||||
2. Delete the API key using its ID
|
||||
- **Note**: SendGrid only shows partial keys in the list, so the first key is extracted
|
||||
|
||||
### 2. Netlify (`netlify.yml`)
|
||||
- **Rule IDs**: `kingfisher.netlify.1`, `kingfisher.netlify.2`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /api/v1/access_tokens/{token_id}`
|
||||
- **Process**:
|
||||
1. List all access tokens to find the current token's ID
|
||||
2. Delete the access token using its ID
|
||||
|
||||
### 3. Tailscale (`tailscale.yml`)
|
||||
- **Rule ID**: `kingfisher.tailscale.1`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /api/v2/key/{keyId}`
|
||||
- **Process**:
|
||||
1. List all keys to find the current key's ID
|
||||
2. Delete the key using its ID
|
||||
|
||||
### 4. ElevenLabs (`elevenlabs.yml`)
|
||||
- **Rule ID**: `kingfisher.elevenlabs.1`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /v1/user/api-keys/{api_key_id}`
|
||||
- **Process**:
|
||||
1. List all API keys to find the current key's ID
|
||||
2. Delete the API key using its ID
|
||||
|
||||
### 5. Sourcegraph (`sourcegraph.yml`)
|
||||
- **Rule IDs**: `kingfisher.sourcegraph.1`, `kingfisher.sourcegraph.2`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: GraphQL mutation `deleteAccessToken`
|
||||
- **Process**:
|
||||
1. Query GraphQL to get the current token's ID
|
||||
2. Execute GraphQL mutation to delete the token
|
||||
- **Note**: Uses GraphQL API instead of REST
|
||||
|
||||
### 6. MongoDB Atlas (`mongodb.yml`)
|
||||
- **Rule ID**: `kingfisher.mongodb.1`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /api/atlas/v2/groups/{GROUP_ID}/apiKeys/{PUBLIC_KEY}`
|
||||
- **Process**:
|
||||
1. List all groups to get the first GROUP_ID (Project ID)
|
||||
2. Delete the API key using the public key as ID
|
||||
- **Authentication**: Uses HTTP Digest authentication
|
||||
- **Note**: The Public Key is the ID needed for deletion
|
||||
|
||||
### 7. Sumo Logic (`sumologic.yml`)
|
||||
- **Rule ID**: `kingfisher.sumologic.2`
|
||||
- **Revocation Type**: Http (single-step)
|
||||
- **Endpoint**: `DELETE /api/v1/accessKeys/{id}`
|
||||
- **Process**: Direct deletion using the Access ID
|
||||
- **Authentication**: Basic Auth (Access ID as username, Access Key as password)
|
||||
- **Note**: The Access ID is the ID needed for deletion (captured from `kingfisher.sumologic.1`)
|
||||
|
||||
### 8. Twilio (`twilio.yml`)
|
||||
- **Rule ID**: `kingfisher.twilio.2`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /2010-04-01/Accounts/{Account_SID}/Keys/{Key_SID}.json`
|
||||
- **Process**:
|
||||
1. List accounts to get the Account SID
|
||||
2. Delete the API key using both Account SID and Key SID
|
||||
- **Note**: Assumes TWILIOID is an API Key SID (starts with `SK`)
|
||||
|
||||
### 9. NPM (`npm.yml`)
|
||||
- **Rule IDs**: `kingfisher.npm.1`, `kingfisher.npm.2`
|
||||
- **Revocation Type**: HttpMultiStep (2-step)
|
||||
- **Endpoint**: `DELETE /-/npm/v1/tokens/token/{token_key}`
|
||||
- **Process**:
|
||||
1. List all tokens to find the current token's key ID
|
||||
2. Revoke the token using its key
|
||||
- **Alternative**: Can also use `npm token revoke <id>` CLI command
|
||||
|
||||
|
||||
## Testing Revocation
|
||||
|
||||
To test revocation for a detected token:
|
||||
|
||||
```bash
|
||||
# Revoke a token using the rule ID
|
||||
kingfisher revoke --rule <rule_id> <token>
|
||||
|
||||
# With debug logging to see step-by-step execution
|
||||
RUST_LOG=debug kingfisher revoke --rule <rule_id> <token>
|
||||
|
||||
# With additional variables if needed (e.g., for services with depends_on_rule)
|
||||
kingfisher revoke --rule <rule_id> --var EXTRA_VAR=value <token>
|
||||
```
|
||||
|
||||
### Example: Revoking a SendGrid API Key
|
||||
|
||||
```bash
|
||||
# Revoke a SendGrid API key
|
||||
kingfisher revoke --rule kingfisher.sendgrid.1 "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"
|
||||
```
|
||||
|
||||
### Example: Revoking a MongoDB API Key
|
||||
|
||||
```bash
|
||||
# Revoke a MongoDB Atlas API key (requires both public and private key)
|
||||
kingfisher revoke --rule kingfisher.mongodb.1 \
|
||||
--var PUBKEY=qj4Zrh8e6A \
|
||||
"4b18315e-6b7d-4337-b449-5d38f5a189ec"
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Multi-Step Revocation Pattern
|
||||
|
||||
All multi-step revocations follow this general pattern:
|
||||
|
||||
```yaml
|
||||
revocation:
|
||||
type: HttpMultiStep
|
||||
content:
|
||||
steps:
|
||||
# Step 1: Lookup
|
||||
- name: lookup_id
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.service.com/endpoint
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
extract:
|
||||
ID_VARIABLE:
|
||||
type: JsonPath
|
||||
path: "$.path.to.id"
|
||||
|
||||
# Step 2: Delete
|
||||
- name: delete
|
||||
request:
|
||||
method: DELETE
|
||||
url: https://api.service.com/endpoint/{{ ID_VARIABLE }}
|
||||
headers:
|
||||
Authorization: "Bearer {{ TOKEN }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
```
|
||||
|
||||
### Variable Extraction Methods
|
||||
|
||||
The following extraction methods are used across different services:
|
||||
|
||||
| Method | Description | Example Services |
|
||||
|--------|-------------|------------------|
|
||||
| **JsonPath** | Extract from JSON response using JSONPath syntax | SendGrid, Netlify, Tailscale, ElevenLabs, NPM, MongoDB |
|
||||
| **Regex** | Extract using regex with a capture group | (Not used in current implementations) |
|
||||
| **Header** | Extract an HTTP response header value | (Not used in current implementations) |
|
||||
| **Body** | Use the entire response body | (Not used in current implementations) |
|
||||
|
||||
### Common JSONPath Patterns
|
||||
|
||||
- `$.result[0].api_key_id` - SendGrid: Extract first API key ID from result array
|
||||
- `$[0].id` - Netlify: Extract ID from root-level array
|
||||
- `$.keys[0].id` - Tailscale: Extract first key ID from keys object
|
||||
- `$.api_keys[0].api_key_id` - ElevenLabs: Extract first API key ID
|
||||
- `$.data.currentUser.accessTokens.nodes[0].id` - Sourcegraph: Extract token ID from nested GraphQL response
|
||||
- `$.results[0].id` - MongoDB: Extract first group ID from results
|
||||
- `$.objects[0].token.key` - NPM: Extract token key from objects array
|
||||
- `$.accounts[0].sid` - Twilio: Extract account SID from accounts array
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Identification
|
||||
|
||||
Some services (like SendGrid and Netlify) list all tokens but don't include the full token value in the response. The current implementations extract the **first** token from the list, which assumes:
|
||||
|
||||
1. The user has only one active token, OR
|
||||
2. The token being revoked is the first one in the list
|
||||
|
||||
**Important**: If multiple tokens exist, the wrong token might be revoked. In production, consider:
|
||||
- Adding user prompts to confirm which token to revoke
|
||||
- Matching tokens by creation date, name, or other metadata
|
||||
- Displaying a list of tokens for user selection
|
||||
|
||||
### Digest Authentication
|
||||
|
||||
MongoDB Atlas uses HTTP Digest authentication, which is properly handled by the Kingfisher HTTP client via the `digest` field in the request configuration.
|
||||
|
||||
### GraphQL APIs
|
||||
|
||||
Sourcegraph uses GraphQL mutations for revocation. The implementation:
|
||||
1. Uses a GraphQL query to get the token ID
|
||||
2. Uses a GraphQL mutation with variables to delete the token
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Maximum 2 steps**: The HttpMultiStep implementation supports only 1-2 steps
|
||||
2. **Sequential execution**: Steps execute in order; no parallel execution
|
||||
3. **Token identification**: Services that don't return full token values may revoke the wrong token if multiple exist
|
||||
4. **Requires API access**: All revocations require the token to have sufficient permissions to list and delete itself
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for revocation support:
|
||||
|
||||
1. **Interactive mode**: Prompt user to select which token to revoke when multiple exist
|
||||
2. **Dry-run mode**: Show what would be revoked without actually revoking
|
||||
3. **Batch revocation**: Revoke multiple tokens at once
|
||||
4. **Revocation history**: Track what was revoked and when
|
||||
5. **Rollback support**: For services that support token restoration
|
||||
6. **Service-specific CLI support**: For services like NPM that have CLI commands
|
||||
|
||||
## References
|
||||
|
||||
- [Multi-Step Revocation Implementation](MULTI_STEP_REVOCATION.md)
|
||||
- [Writing Custom Rules](RULES.md)
|
||||
- [Kingfisher Rules Schema](../crates/kingfisher-rules/src/rule.rs)
|
||||
|
|
@ -21,13 +21,16 @@ use crate::{
|
|||
cli::{commands::revoke::RevokeArgs, global::GlobalArgs},
|
||||
liquid_filters::register_all,
|
||||
rule_loader::RuleLoader,
|
||||
rules::{rule::Rule, HttpValidation, Revocation},
|
||||
validation::aws::{revoke_aws_access_key, validate_aws_credentials_input},
|
||||
validation::gcp::revoke_gcp_service_account_key,
|
||||
validation::httpvalidation::{build_request_builder, retry_request, validate_response},
|
||||
validation::GLOBAL_USER_AGENT,
|
||||
};
|
||||
|
||||
use kingfisher_rules::{
|
||||
HttpMultiStepRevocation, HttpValidation, ResponseExtractor, Revocation, RevocationStep, Rule,
|
||||
};
|
||||
|
||||
/// Result of a direct revocation attempt.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DirectRevocationResult {
|
||||
|
|
@ -113,6 +116,21 @@ fn extract_revocation_vars(revocation: &Revocation) -> BTreeSet<String> {
|
|||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
Revocation::HttpMultiStep(multi_step) => {
|
||||
// Extract variables from all steps
|
||||
// Note: Variables extracted in step 1 are available in step 2,
|
||||
// but we only track initial input variables here
|
||||
for step in &multi_step.steps {
|
||||
vars.extend(extract_template_vars(&step.request.url));
|
||||
for (key, value) in &step.request.headers {
|
||||
vars.extend(extract_template_vars(key));
|
||||
vars.extend(extract_template_vars(value));
|
||||
}
|
||||
if let Some(body) = &step.request.body {
|
||||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vars
|
||||
|
|
@ -191,6 +209,70 @@ async fn render_and_parse_url(
|
|||
reqwest::Url::parse(&rendered).map_err(|e| anyhow!("Invalid URL '{}': {}", rendered, e))
|
||||
}
|
||||
|
||||
/// Extract a value from an HTTP response using the specified extractor.
|
||||
fn extract_value_from_response(
|
||||
extractor: &ResponseExtractor,
|
||||
body: &str,
|
||||
headers: &reqwest::header::HeaderMap,
|
||||
status: &reqwest::StatusCode,
|
||||
) -> Result<String> {
|
||||
match extractor {
|
||||
ResponseExtractor::JsonPath { path } => {
|
||||
let json: serde_json::Value =
|
||||
serde_json::from_str(body).context("Response body is not valid JSON")?;
|
||||
|
||||
// Simple JSONPath implementation supporting basic paths like:
|
||||
// $.field, $.field.nested, $.array[0], $.array[0].field
|
||||
let path_parts: Vec<&str> = path.trim_start_matches("$.").split('.').collect();
|
||||
|
||||
let mut current = &json;
|
||||
for part in path_parts {
|
||||
if let Some((array_name, index_str)) = part.split_once('[') {
|
||||
let index: usize =
|
||||
index_str.trim_end_matches(']').parse().context("Invalid array index")?;
|
||||
|
||||
if !array_name.is_empty() {
|
||||
current = current
|
||||
.get(array_name)
|
||||
.ok_or_else(|| anyhow!("Field '{}' not found", array_name))?;
|
||||
}
|
||||
|
||||
current = current
|
||||
.get(index)
|
||||
.ok_or_else(|| anyhow!("Array index {} not found", index))?;
|
||||
} else {
|
||||
current =
|
||||
current.get(part).ok_or_else(|| anyhow!("Field '{}' not found", part))?;
|
||||
}
|
||||
}
|
||||
|
||||
match current {
|
||||
serde_json::Value::String(s) => Ok(s.clone()),
|
||||
serde_json::Value::Number(n) => Ok(n.to_string()),
|
||||
serde_json::Value::Bool(b) => Ok(b.to_string()),
|
||||
_ => Ok(current.to_string()),
|
||||
}
|
||||
}
|
||||
ResponseExtractor::Regex { pattern } => {
|
||||
let re = Regex::new(pattern).context(format!("Invalid regex pattern: {}", pattern))?;
|
||||
let caps = re
|
||||
.captures(body)
|
||||
.ok_or_else(|| anyhow!("Regex pattern did not match response body"))?;
|
||||
|
||||
caps.get(1)
|
||||
.map(|m| m.as_str().to_string())
|
||||
.ok_or_else(|| anyhow!("No capture group found in regex pattern"))
|
||||
}
|
||||
ResponseExtractor::Header { name } => headers
|
||||
.get(name)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| anyhow!("Header '{}' not found in response", name)),
|
||||
ResponseExtractor::Body => Ok(body.to_string()),
|
||||
ResponseExtractor::StatusCode => Ok(status.as_u16().to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute HTTP revocation against the provided rule.
|
||||
async fn execute_http_revocation(
|
||||
http_revocation: &HttpValidation,
|
||||
|
|
@ -247,6 +329,137 @@ async fn execute_http_revocation(
|
|||
})
|
||||
}
|
||||
|
||||
/// Execute a single revocation step and extract variables from the response.
|
||||
async fn execute_revocation_step(
|
||||
step: &RevocationStep,
|
||||
globals: &mut Object,
|
||||
client: &Client,
|
||||
parser: &liquid::Parser,
|
||||
timeout: Duration,
|
||||
retries: u32,
|
||||
step_number: usize,
|
||||
) -> Result<(reqwest::StatusCode, String)> {
|
||||
let default_step_name = format!("step_{}", step_number);
|
||||
let step_name = step.name.as_ref().map(|s| s.as_str()).unwrap_or(&default_step_name);
|
||||
|
||||
debug!("Executing revocation step {}: {}", step_number, step_name);
|
||||
|
||||
let url = render_and_parse_url(parser, globals, &step.request.url).await?;
|
||||
debug!("Step {} URL: {}", step_number, url);
|
||||
|
||||
let request_builder = build_request_builder(
|
||||
client,
|
||||
&step.request.method,
|
||||
&url,
|
||||
&step.request.headers,
|
||||
&step.request.body,
|
||||
timeout,
|
||||
parser,
|
||||
globals,
|
||||
)
|
||||
.map_err(|e| anyhow!("Failed to build request for {}: {}", step_name, e))?;
|
||||
|
||||
let backoff_min = Duration::from_millis(100);
|
||||
let backoff_max = Duration::from_secs(2);
|
||||
|
||||
let response = retry_request(request_builder, retries, backoff_min, backoff_max)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Request failed for {}: {}", step_name, e))?;
|
||||
|
||||
let status = response.status();
|
||||
let headers = response.headers().clone();
|
||||
let body =
|
||||
response.text().await.unwrap_or_else(|e| format!("Failed to read response body: {}", e));
|
||||
|
||||
// Extract variables from the response if configured
|
||||
if let Some(extractors) = &step.extract {
|
||||
debug!("Extracting {} variable(s) from step {} response", extractors.len(), step_number);
|
||||
|
||||
for (var_name, extractor) in extractors {
|
||||
match extract_value_from_response(extractor, &body, &headers, &status) {
|
||||
Ok(value) => {
|
||||
debug!("Step {}: Extracted variable {} = '{}'", step_number, var_name, value);
|
||||
globals.insert(var_name.to_uppercase().into(), Value::scalar(value));
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow!(
|
||||
"Failed to extract variable '{}' in step {}: {}",
|
||||
var_name,
|
||||
step_number,
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((status, body))
|
||||
}
|
||||
|
||||
/// Execute multi-step HTTP revocation.
|
||||
async fn execute_multi_step_revocation(
|
||||
multi_step: &HttpMultiStepRevocation,
|
||||
globals: &mut Object,
|
||||
client: &Client,
|
||||
parser: &liquid::Parser,
|
||||
timeout: Duration,
|
||||
retries: u32,
|
||||
) -> Result<DirectRevocationResult> {
|
||||
if multi_step.steps.is_empty() {
|
||||
bail!("Multi-step revocation must have at least one step");
|
||||
}
|
||||
|
||||
if multi_step.steps.len() > 2 {
|
||||
bail!(
|
||||
"Multi-step revocation supports a maximum of 2 steps, got {}",
|
||||
multi_step.steps.len()
|
||||
);
|
||||
}
|
||||
|
||||
let num_steps = multi_step.steps.len();
|
||||
debug!("Executing {}-step revocation", num_steps);
|
||||
|
||||
// Execute each step sequentially
|
||||
for (i, step) in multi_step.steps.iter().enumerate() {
|
||||
let step_number = i + 1;
|
||||
let is_final_step = step_number == num_steps;
|
||||
|
||||
let (status, body) =
|
||||
execute_revocation_step(step, globals, client, parser, timeout, retries, step_number)
|
||||
.await?;
|
||||
|
||||
if is_final_step {
|
||||
// Final step: validate response to determine success
|
||||
let display_body =
|
||||
if body.len() > 500 { format!("{}...", &body[..500]) } else { body.clone() };
|
||||
|
||||
let matchers = step
|
||||
.request
|
||||
.response_matcher
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow!("Final revocation step must have response_matcher"))?;
|
||||
|
||||
let headers = reqwest::header::HeaderMap::new();
|
||||
let html_allowed = step.request.response_is_html;
|
||||
let revoked = validate_response(matchers, &body, &status, &headers, html_allowed);
|
||||
|
||||
return Ok(DirectRevocationResult {
|
||||
rule_id: String::new(),
|
||||
rule_name: String::new(),
|
||||
revoked,
|
||||
status_code: Some(status.as_u16()),
|
||||
message: display_body,
|
||||
});
|
||||
} else {
|
||||
// Intermediate step: just log the response
|
||||
debug!("Step {} completed with status {}", step_number, status);
|
||||
}
|
||||
}
|
||||
|
||||
// This should never happen due to the checks above, but keep for safety
|
||||
Err(anyhow!("Multi-step revocation did not complete"))
|
||||
}
|
||||
|
||||
/// Run direct revocation of a secret against one or more rules.
|
||||
pub async fn run_direct_revocation(
|
||||
args: &RevokeArgs,
|
||||
|
|
@ -405,6 +618,18 @@ pub async fn run_direct_revocation(
|
|||
)
|
||||
.await?
|
||||
}
|
||||
Revocation::HttpMultiStep(multi_step) => {
|
||||
let mut globals_mut = globals.clone();
|
||||
execute_multi_step_revocation(
|
||||
multi_step,
|
||||
&mut globals_mut,
|
||||
&client,
|
||||
&parser,
|
||||
timeout,
|
||||
args.retries,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
result.rule_id = rule_id;
|
||||
|
|
|
|||
|
|
@ -121,6 +121,15 @@ fn build_revoke_command(
|
|||
escape_for_shell(snippet)
|
||||
))
|
||||
}
|
||||
Revocation::HttpMultiStep(_) => {
|
||||
// Multi-step HTTP revocation with dependent variables
|
||||
Some(format!(
|
||||
"kingfisher revoke --rule {} {}{}",
|
||||
rule_id,
|
||||
var_args,
|
||||
escape_for_shell(snippet)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue