forked from mirrors/kingfisher
- Added Vercel credential rules for new token formats introduced February 2026: vcp_ (personal access), vci_ (integration), vca_ (app access), vcr_ (app refresh), vck_ (AI Gateway API key). All use CRC32/Base62 checksum validation. Legacy 24-char format retained as kingfisher.vercel.1.
- Added revocation support for Vercel app tokens (vca_, vcr_) via https://api.vercel.com/login/oauth/token/revoke. Requires VERCEL_APP_CLIENT_ID (or NEXT_PUBLIC_VERCEL_APP_CLIENT_ID) and VERCEL_APP_CLIENT_SECRET. - Fixed validate/revoke command generation to omit regex named captures (e.g., BODY, CHECKSUM) when they are not used by validation/revocation templates, so rules like Vercel no longer produce unnecessary --var BODY=... arguments.
This commit is contained in:
parent
265e569c60
commit
4ab5932d57
5 changed files with 455 additions and 14 deletions
|
|
@ -3,6 +3,10 @@
|
|||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.82.0]
|
||||
- Added Vercel credential rules for new token formats introduced February 2026: `vcp_` (personal access), `vci_` (integration), `vca_` (app access), `vcr_` (app refresh), `vck_` (AI Gateway API key). All use CRC32/Base62 checksum validation. Legacy 24-char format retained as `kingfisher.vercel.1`.
|
||||
- Added revocation support for Vercel app tokens (`vca_`, `vcr_`) via `https://api.vercel.com/login/oauth/token/revoke`. Requires `VERCEL_APP_CLIENT_ID` (or `NEXT_PUBLIC_VERCEL_APP_CLIENT_ID`) and `VERCEL_APP_CLIENT_SECRET`.
|
||||
- Fixed validate/revoke command generation to omit regex named captures (e.g., `BODY`, `CHECKSUM`) when they are not used by validation/revocation templates, so rules like Vercel no longer produce unnecessary `--var BODY=...` arguments.
|
||||
- Fixed HTTP validation incorrectly marking valid credentials as inactive when response bodies exceeded 2048 bytes. Matchers (`JsonValid`, `WordMatch`, etc.) now run against the full response; only the stored preview remains truncated for reporting.
|
||||
- Fixed validation flakiness under service rate limiting by retrying HTTP validations on 429/408 in addition to transient 5xx failures.
|
||||
- Prevented transient HTTP validation failures (429/5xx) from being cached, avoiding cache poisoning that could suppress later successful validations in the same scan.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
rules:
|
||||
- name: Vercel API Token
|
||||
- name: Vercel API Token (legacy 24-char)
|
||||
id: kingfisher.vercel.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
|
|
@ -35,9 +35,260 @@ rules:
|
|||
- '"email":'
|
||||
match_all_words: true
|
||||
references:
|
||||
- https://vercel.com/docs/rest-api#authentication
|
||||
- https://vercel.com/kb/guide/how-do-i-use-a-vercel-api-access-token
|
||||
- https://docs.vercel.com/docs/rest-api/reference/welcome#authentication
|
||||
examples:
|
||||
- "vercel-key = DdZV6ZDZW6Vpl7n7JqtrCE5i"
|
||||
- "vercel_token = zyMBA1qVEMAf4UNNZtCAbg6u"
|
||||
- "vercel_api_key = MTg0AW799OY1HmyDdn84or3C"
|
||||
- "vercel_secret = A7n9Xfp3tBz7D0XpOTMWpiOM"
|
||||
|
||||
- name: Vercel Personal Access Token (vcp_)
|
||||
id: kingfisher.vercel.2
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
vcp_(?P<body>[A-Za-z0-9_-]{50})(?P<checksum>[A-Za-z0-9]{6})
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
checksum:
|
||||
actual:
|
||||
template: "{{ checksum }}"
|
||||
requires_capture: checksum
|
||||
# Vercel token checksum format: Base62(CRC32(body)) padded to 6 chars.
|
||||
# The "body" is the 50-char payload portion after the prefix.
|
||||
expected: "{{ body | crc32 | base62: 6 }}"
|
||||
skip_if_missing: true
|
||||
confidence: medium
|
||||
min_entropy: 3.5
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.vercel.com/v2/user
|
||||
headers:
|
||||
Authorization: "Bearer {{TOKEN}}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"user":'
|
||||
- '"email":'
|
||||
match_all_words: true
|
||||
references:
|
||||
- https://vercel.com/kb/guide/how-do-i-use-a-vercel-api-access-token
|
||||
- https://docs.vercel.com/docs/rest-api/reference/welcome#authentication
|
||||
- https://vercel.com/changelog/new-token-formats-and-secret-scanning
|
||||
examples:
|
||||
- "vcp_35UYJwYZDigYATKhxJUAhPqRhit2Xe3dtiG60LsUTHeklEXDQ94Jafpu"
|
||||
- "vercel_access_token=vcp_4mcjwVDwqtVCVGWCcxRjdzGpkGZ3NkwXZv8ktcoQ0EG0dnjpMP1Rzi71"
|
||||
|
||||
- name: Vercel Integration Token (vci_)
|
||||
id: kingfisher.vercel.3
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
vci_(?P<body>[A-Za-z0-9_-]{50})(?P<checksum>[A-Za-z0-9]{6})
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
checksum:
|
||||
actual:
|
||||
template: "{{ checksum }}"
|
||||
requires_capture: checksum
|
||||
expected: "{{ body | crc32 | base62: 6 }}"
|
||||
skip_if_missing: true
|
||||
confidence: medium
|
||||
min_entropy: 3.5
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://api.vercel.com/v2/user
|
||||
headers:
|
||||
Authorization: "Bearer {{TOKEN}}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"user":'
|
||||
match_all_words: false
|
||||
references:
|
||||
- https://vercel.com/docs/integrations/vercel-api-integrations#create-an-access-token
|
||||
- https://vercel.com/changelog/new-token-formats-and-secret-scanning
|
||||
examples:
|
||||
- "Vercel Integration Token: vci_35UYJwYZDigYATKhxJUAhPqRhit2Xe3dtiG60LsUTHeklEXDQ94Jafpu"
|
||||
|
||||
- name: Vercel App Access Token (vca_)
|
||||
id: kingfisher.vercel.4
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
vca_(?P<body>[A-Za-z0-9_-]{50})(?P<checksum>[A-Za-z0-9]{6})
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
checksum:
|
||||
actual:
|
||||
template: "{{ checksum }}"
|
||||
requires_capture: checksum
|
||||
expected: "{{ body | crc32 | base62: 6 }}"
|
||||
skip_if_missing: true
|
||||
confidence: medium
|
||||
min_entropy: 3.5
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.vercel.com/login/oauth/userinfo
|
||||
headers:
|
||||
Authorization: "Bearer {{TOKEN}}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"sub":'
|
||||
match_all_words: true
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.vercel.com/login/oauth/token/revoke
|
||||
headers:
|
||||
# Requires the Vercel App's client_id and client_secret (Sign in with Vercel).
|
||||
# Docs use NEXT_PUBLIC_VERCEL_APP_CLIENT_ID; support both.
|
||||
Authorization: "Basic {{ NEXT_PUBLIC_VERCEL_APP_CLIENT_ID | default: VERCEL_APP_CLIENT_ID | append: ':' | append: VERCEL_APP_CLIENT_SECRET | b64enc }}"
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: "token={{ TOKEN | url_encode }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
references:
|
||||
- https://vercel.com/docs/sign-in-with-vercel/tokens#access-token
|
||||
- https://vercel.com/docs/sign-in-with-vercel/authorization-server-api#user-info-endpoint
|
||||
- https://vercel.com/docs/sign-in-with-vercel/authorization-server-api#revoke-token-endpoint
|
||||
- https://vercel.com/changelog/new-token-formats-and-secret-scanning
|
||||
examples:
|
||||
- "vca_BQuu9ChDu3n6Pfh6YQnCshpoYkWDSFKogLqmBtQ0tC8NAA5rXt340sjz"
|
||||
|
||||
- name: Vercel App Refresh Token (vcr_)
|
||||
id: kingfisher.vercel.5
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
vcr_(?P<body>[A-Za-z0-9_-]{50})(?P<checksum>[A-Za-z0-9]{6})
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
checksum:
|
||||
actual:
|
||||
template: "{{ checksum }}"
|
||||
requires_capture: checksum
|
||||
expected: "{{ body | crc32 | base62: 6 }}"
|
||||
skip_if_missing: true
|
||||
confidence: medium
|
||||
min_entropy: 3.5
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.vercel.com/login/oauth/token/introspect
|
||||
headers:
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: "token={{ TOKEN | url_encode }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"active":true'
|
||||
match_all_words: true
|
||||
revocation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: POST
|
||||
url: https://api.vercel.com/login/oauth/token/revoke
|
||||
headers:
|
||||
Authorization: "Basic {{ NEXT_PUBLIC_VERCEL_APP_CLIENT_ID | default: VERCEL_APP_CLIENT_ID | append: ':' | append: VERCEL_APP_CLIENT_SECRET | b64enc }}"
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
body: "token={{ TOKEN | url_encode }}"
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200, 204]
|
||||
references:
|
||||
- https://vercel.com/docs/sign-in-with-vercel/tokens#refresh-token
|
||||
- https://vercel.com/docs/sign-in-with-vercel/authorization-server-api#token-introspection-endpoint
|
||||
- https://vercel.com/docs/sign-in-with-vercel/authorization-server-api#revoke-token-endpoint
|
||||
- https://vercel.com/changelog/new-token-formats-and-secret-scanning
|
||||
examples:
|
||||
- "vcr_BQuu9ChDu3n6Pfh6YQnCshpoYkWDSFKogLqmBtQ0tC8NAA5rXt340sjz"
|
||||
|
||||
- name: Vercel AI Gateway API Key (vck_)
|
||||
id: kingfisher.vercel.6
|
||||
pattern: |
|
||||
(?x)
|
||||
\b
|
||||
(
|
||||
vck_(?P<body>[A-Za-z0-9_-]{50})(?P<checksum>[A-Za-z0-9]{6})
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 3
|
||||
checksum:
|
||||
actual:
|
||||
template: "{{ checksum }}"
|
||||
requires_capture: checksum
|
||||
expected: "{{ body | crc32 | base62: 6 }}"
|
||||
skip_if_missing: true
|
||||
confidence: medium
|
||||
min_entropy: 3.5
|
||||
validation:
|
||||
type: Http
|
||||
content:
|
||||
request:
|
||||
method: GET
|
||||
url: https://ai-gateway.vercel.sh/v1/models
|
||||
headers:
|
||||
Authorization: "Bearer {{TOKEN}}"
|
||||
Content-Type: application/json
|
||||
response_matcher:
|
||||
- report_response: true
|
||||
- type: StatusMatch
|
||||
status: [200]
|
||||
- type: JsonValid
|
||||
- type: WordMatch
|
||||
words:
|
||||
- '"data"'
|
||||
match_all_words: true
|
||||
references:
|
||||
- https://vercel.com/docs/ai-gateway/authentication-and-byok/authentication
|
||||
- https://vercel.com/docs/ai-gateway/openai-compat/rest-api
|
||||
- https://vercel.com/changelog/new-token-formats-and-secret-scanning
|
||||
examples:
|
||||
- "vck_2YkmQj1uHqCVNoUx5a9uvRTe81gmAcln5hoRMPWFBU4tulufUf0OzP2K"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This document provides an overview of the revocation support added to Kingfisher
|
|||
|
||||
## Overview
|
||||
|
||||
Revocation support has been added for **6 services** that provide verified, documented programmatic API endpoints to delete or revoke access tokens/keys. Most implementations use the **HttpMultiStep** revocation type because they require a two-step process:
|
||||
Revocation support has been added for **7 services** that provide verified, documented 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
|
||||
|
|
@ -66,6 +66,14 @@ Revocation support has been added for **6 services** that provide verified, docu
|
|||
2. Revoke the token using its key
|
||||
- **Alternative**: Can also use `npm token revoke <id>` CLI command
|
||||
|
||||
### 7. Vercel (Sign in with Vercel) (`vercel.yml`)
|
||||
|
||||
- **Rule IDs**: `kingfisher.vercel.3` (App Access Token `vca_...`), `kingfisher.vercel.4` (App Refresh Token `vcr_...`)
|
||||
- **Revocation Type**: Http (single-step)
|
||||
- **Endpoint**: `POST https://api.vercel.com/login/oauth/token/revoke`
|
||||
- **Authentication**: HTTP Basic using the Vercel App's `client_id:client_secret`
|
||||
- **Required vars**: `VERCEL_APP_CLIENT_ID`, `VERCEL_APP_CLIENT_SECRET`
|
||||
- **Note**: This is the Authorization Server API used by "Sign in with Vercel" and is separate from the general REST API access tokens (PATs).
|
||||
|
||||
## Testing Revocation
|
||||
|
||||
|
|
|
|||
191
src/reporter.rs
191
src/reporter.rs
|
|
@ -44,24 +44,135 @@ fn escape_for_shell(s: &str) -> String {
|
|||
format!("'{}'", s.replace('\'', "'\\''"))
|
||||
}
|
||||
|
||||
/// Build the --var arguments string from dependent captures.
|
||||
fn extract_template_vars(text: &str) -> BTreeSet<String> {
|
||||
// Match {{ VAR }} or {{ VAR | filter }} patterns; return VAR uppercased.
|
||||
let re = regex::Regex::new(r"\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\|[^}]*)?\}\}")
|
||||
.expect("template var regex should compile");
|
||||
re.captures_iter(text)
|
||||
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_uppercase()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn required_vars_for_validation(validation: &crate::rules::Validation) -> BTreeSet<String> {
|
||||
use crate::rules::Validation;
|
||||
let mut vars = BTreeSet::new();
|
||||
|
||||
match validation {
|
||||
Validation::Http(http) => {
|
||||
vars.extend(extract_template_vars(&http.request.url));
|
||||
for (k, v) in &http.request.headers {
|
||||
vars.extend(extract_template_vars(k));
|
||||
vars.extend(extract_template_vars(v));
|
||||
}
|
||||
if let Some(body) = &http.request.body {
|
||||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
Validation::Grpc(grpc) => {
|
||||
vars.extend(extract_template_vars(&grpc.request.url));
|
||||
for (k, v) in &grpc.request.headers {
|
||||
vars.extend(extract_template_vars(k));
|
||||
vars.extend(extract_template_vars(v));
|
||||
}
|
||||
if let Some(body) = &grpc.request.body {
|
||||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
Validation::AWS => {
|
||||
vars.insert("AKID".to_string());
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
Validation::GCP => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
Validation::MongoDB
|
||||
| Validation::MySQL
|
||||
| Validation::Postgres
|
||||
| Validation::Jdbc
|
||||
| Validation::JWT => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
Validation::AzureStorage => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
vars.insert("AZURENAME".to_string());
|
||||
}
|
||||
Validation::Coinbase => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
vars.insert("CRED_NAME".to_string());
|
||||
}
|
||||
Validation::Raw(_) => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
vars
|
||||
}
|
||||
|
||||
fn required_vars_for_revocation(revocation: &Revocation) -> BTreeSet<String> {
|
||||
let mut vars = BTreeSet::new();
|
||||
|
||||
match revocation {
|
||||
Revocation::AWS => {
|
||||
vars.insert("AKID".to_string());
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
Revocation::GCP => {
|
||||
vars.insert("TOKEN".to_string());
|
||||
}
|
||||
Revocation::Http(http) => {
|
||||
vars.extend(extract_template_vars(&http.request.url));
|
||||
for (k, v) in &http.request.headers {
|
||||
vars.extend(extract_template_vars(k));
|
||||
vars.extend(extract_template_vars(v));
|
||||
}
|
||||
if let Some(body) = &http.request.body {
|
||||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
Revocation::HttpMultiStep(multi) => {
|
||||
for step in &multi.steps {
|
||||
vars.extend(extract_template_vars(&step.request.url));
|
||||
for (k, v) in &step.request.headers {
|
||||
vars.extend(extract_template_vars(k));
|
||||
vars.extend(extract_template_vars(v));
|
||||
}
|
||||
if let Some(body) = &step.request.body {
|
||||
vars.extend(extract_template_vars(body));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vars
|
||||
}
|
||||
|
||||
/// Build the --var arguments string from dependent captures, but only for variables that are
|
||||
/// required by the validation/revocation templates.
|
||||
fn build_var_args(
|
||||
dependent_captures: &std::collections::BTreeMap<String, String>,
|
||||
akid_from_captures: Option<&str>,
|
||||
akid_from_validation_body: Option<&str>,
|
||||
required_vars: &BTreeSet<String>,
|
||||
) -> String {
|
||||
let mut var_args = Vec::new();
|
||||
|
||||
// Add AKID if available (for AWS)
|
||||
if let Some(akid) = akid_from_captures.or(akid_from_validation_body) {
|
||||
if !akid.is_empty() && !dependent_captures.contains_key("AKID") {
|
||||
if !akid.is_empty()
|
||||
&& required_vars.contains("AKID")
|
||||
&& !dependent_captures.contains_key("AKID")
|
||||
{
|
||||
var_args.push(format!("--var AKID={}", escape_for_shell(akid)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add all dependent captures as --var arguments
|
||||
// Add dependent captures only when required by the templates.
|
||||
// This avoids generating commands like `--var BODY=...` for tokens whose named captures
|
||||
// are just internal parsing aids (e.g., checksum payloads).
|
||||
for (name, value) in dependent_captures {
|
||||
var_args.push(format!("--var {}={}", name, escape_for_shell(value)));
|
||||
if required_vars.contains(name) && !name.eq_ignore_ascii_case("TOKEN") {
|
||||
var_args.push(format!("--var {}={}", name, escape_for_shell(value)));
|
||||
}
|
||||
}
|
||||
|
||||
if var_args.is_empty() {
|
||||
|
|
@ -85,8 +196,13 @@ fn build_revoke_command(
|
|||
akid_from_captures: Option<&str>,
|
||||
akid_from_validation_body: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let var_args =
|
||||
build_var_args(dependent_captures, akid_from_captures, akid_from_validation_body);
|
||||
let required_vars = required_vars_for_revocation(revocation);
|
||||
let var_args = build_var_args(
|
||||
dependent_captures,
|
||||
akid_from_captures,
|
||||
akid_from_validation_body,
|
||||
&required_vars,
|
||||
);
|
||||
|
||||
match revocation {
|
||||
Revocation::AWS => {
|
||||
|
|
@ -146,8 +262,13 @@ fn build_validate_command(
|
|||
) -> Option<String> {
|
||||
use crate::rules::Validation;
|
||||
|
||||
let var_args =
|
||||
build_var_args(dependent_captures, akid_from_captures, akid_from_validation_body);
|
||||
let required_vars = required_vars_for_validation(validation);
|
||||
let var_args = build_var_args(
|
||||
dependent_captures,
|
||||
akid_from_captures,
|
||||
akid_from_validation_body,
|
||||
&required_vars,
|
||||
);
|
||||
|
||||
match validation {
|
||||
Validation::AWS => {
|
||||
|
|
@ -1164,9 +1285,63 @@ mod tests {
|
|||
};
|
||||
use gix::{date::Time, ObjectId};
|
||||
use smallvec::SmallVec;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn build_var_args_ignores_unrequired_named_captures() {
|
||||
let dependent = BTreeMap::from([
|
||||
("BODY".to_string(), "payload-part".to_string()),
|
||||
("CHECKSUM".to_string(), "abc123".to_string()),
|
||||
]);
|
||||
let required = BTreeSet::from(["TOKEN".to_string()]);
|
||||
|
||||
let args = build_var_args(&dependent, None, None, &required);
|
||||
assert_eq!(args, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_validate_command_omits_body_checksum_vars_for_vercel_like_http_rule() {
|
||||
let validation = crate::rules::Validation::Http(crate::rules::HttpValidation {
|
||||
request: crate::rules::HttpRequest {
|
||||
method: "GET".to_string(),
|
||||
url: "https://api.vercel.com/v2/user".to_string(),
|
||||
headers: BTreeMap::from([(
|
||||
"Authorization".to_string(),
|
||||
"Bearer {{TOKEN}}".to_string(),
|
||||
)]),
|
||||
body: None,
|
||||
response_matcher: None,
|
||||
multipart: None,
|
||||
response_is_html: false,
|
||||
},
|
||||
multipart: None,
|
||||
});
|
||||
let dependent = BTreeMap::from([
|
||||
("BODY".to_string(), "payload-part".to_string()),
|
||||
("CHECKSUM".to_string(), "abc123".to_string()),
|
||||
]);
|
||||
|
||||
let cmd = build_validate_command(
|
||||
"kingfisher.vercel.1",
|
||||
&validation,
|
||||
"vcp_testtoken",
|
||||
&dependent,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("validate command should be generated");
|
||||
|
||||
assert!(!cmd.contains("--var BODY="), "command should not include BODY var: {}", cmd);
|
||||
assert!(
|
||||
!cmd.contains("--var CHECKSUM="),
|
||||
"command should not include CHECKSUM var: {}",
|
||||
cmd
|
||||
);
|
||||
assert!(cmd.contains("kingfisher validate --rule kingfisher.vercel.1"));
|
||||
}
|
||||
|
||||
fn sample_scan_args() -> ScanArgs {
|
||||
ScanArgs {
|
||||
num_jobs: 1,
|
||||
|
|
|
|||
|
|
@ -675,7 +675,7 @@ async fn timed_validate_single_match<'a>(
|
|||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let headers = resp.headers().clone();
|
||||
let mut body = match resp.text().await {
|
||||
let body = match resp.text().await {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
m.validation_success = false;
|
||||
|
|
@ -688,10 +688,13 @@ async fn timed_validate_single_match<'a>(
|
|||
return;
|
||||
}
|
||||
};
|
||||
truncate_to_char_boundary(&mut body, MAX_VALIDATION_BODY_LEN);
|
||||
// Validate against the full response body, but keep a truncated preview for
|
||||
// reporting/storage to avoid huge outputs.
|
||||
let mut display_body = body.clone();
|
||||
truncate_to_char_boundary(&mut display_body, MAX_VALIDATION_BODY_LEN);
|
||||
|
||||
m.validation_response_status = status;
|
||||
let body_opt = validation_body::from_string(body.clone());
|
||||
let body_opt = validation_body::from_string(display_body.clone());
|
||||
m.validation_response_body = body_opt.clone();
|
||||
let matchers = match http_validation.request.response_matcher.as_ref() {
|
||||
Some(m) => m,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue