0e8f1ca03a
Добавить структурированные журналы, метрики, трассировку и безопасный канал критических ошибок. Усилить границы рантайма, тесты, проверку зависимостей и сценарии развёртывания.
207 lines
5.8 KiB
Rust
207 lines
5.8 KiB
Rust
use crank_observability::{RedactionLimits, redact_value};
|
|
use serde_json::{Value, json};
|
|
|
|
const REDACTED: &str = "[REDACTED]";
|
|
const TRUNCATED: &str = "[TRUNCATED]";
|
|
|
|
#[test]
|
|
fn sensitive_keys_are_redacted_case_insensitively() {
|
|
let keys = [
|
|
"password",
|
|
"PassWord",
|
|
"api_key",
|
|
"access-token",
|
|
"authorization",
|
|
"Proxy.Authorization",
|
|
"cookie",
|
|
"set_cookie",
|
|
"payload",
|
|
"request_body",
|
|
"arguments",
|
|
"result",
|
|
"response",
|
|
];
|
|
|
|
for key in keys {
|
|
let cleaned = redact_value(&json!({ key: "canary-secret" }), RedactionLimits::default());
|
|
assert_eq!(cleaned[key], REDACTED, "key {key} was not redacted");
|
|
assert!(!cleaned.to_string().contains("canary-secret"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn generated_sensitive_key_variants_are_redacted() {
|
|
for canonical in [
|
|
"password",
|
|
"api_key",
|
|
"access_key",
|
|
"secret_key",
|
|
"proxy_authorization",
|
|
"set_cookie",
|
|
"query_string",
|
|
"request_body",
|
|
] {
|
|
for separator in ["_", "-", "."] {
|
|
let variant = canonical
|
|
.split('_')
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
.to_ascii_uppercase();
|
|
let cleaned = redact_value(
|
|
&json!({ variant.clone(): "canary-secret" }),
|
|
RedactionLimits::default(),
|
|
);
|
|
|
|
assert_eq!(
|
|
cleaned[&variant], REDACTED,
|
|
"key {variant} was not redacted"
|
|
);
|
|
assert!(!cleaned.to_string().contains("canary-secret"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn compound_sensitive_key_names_are_redacted() {
|
|
for key in [
|
|
"client_api_key",
|
|
"aws_access_key_id",
|
|
"http_authorization_header",
|
|
"response_body",
|
|
"tool_arguments",
|
|
"query_params",
|
|
"secret_value",
|
|
] {
|
|
let cleaned = redact_value(&json!({ key: "canary-secret" }), RedactionLimits::default());
|
|
|
|
assert_eq!(cleaned[key], REDACTED, "key {key} was not redacted");
|
|
assert!(!cleaned.to_string().contains("canary-secret"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn url_query_and_fragment_are_removed_at_every_depth() {
|
|
let input = json!({
|
|
"url": "https://example.test/path?token=canary-secret#fragment",
|
|
"nested": [{
|
|
"endpoint_uri": "https://example.test/other?q=canary-secret"
|
|
}]
|
|
});
|
|
|
|
let cleaned = redact_value(&input, RedactionLimits::default());
|
|
|
|
assert_eq!(cleaned["url"], "https://example.test/path");
|
|
assert_eq!(
|
|
cleaned["nested"][0]["endpoint_uri"],
|
|
"https://example.test/other"
|
|
);
|
|
assert!(!cleaned.to_string().contains("canary-secret"));
|
|
}
|
|
|
|
#[test]
|
|
fn url_credentials_and_compound_endpoint_fields_are_removed() {
|
|
let input = json!({
|
|
"upstream_endpoint": "https://user:canary-secret@example.test/path?token=canary-secret",
|
|
});
|
|
|
|
let cleaned = redact_value(&input, RedactionLimits::default());
|
|
|
|
assert_eq!(cleaned["upstream_endpoint"], "https://example.test/path");
|
|
assert!(!cleaned.to_string().contains("user"));
|
|
assert!(!cleaned.to_string().contains("canary-secret"));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_values_and_collections_respect_all_limits() {
|
|
let limits = RedactionLimits {
|
|
max_string_bytes: 16,
|
|
max_array_items: 3,
|
|
max_object_fields: 3,
|
|
max_depth: 2,
|
|
max_event_bytes: 256,
|
|
};
|
|
let input = json!({
|
|
"long": "абвгдежзийклмнопрсту",
|
|
"array": [1, 2, 3, 4, 5],
|
|
"object": {"a": 1, "b": 2, "c": 3, "d": 4},
|
|
"nested": {"level2": {"level3": "must not survive"}}
|
|
});
|
|
|
|
let cleaned = redact_value(&input, limits);
|
|
let object = cleaned
|
|
.as_object()
|
|
.expect("cleaned root must remain an object");
|
|
|
|
assert!(object.len() <= limits.max_object_fields);
|
|
assert!(
|
|
cleaned["long"]
|
|
.as_str()
|
|
.map(|value| value.len() <= limits.max_string_bytes)
|
|
.unwrap_or(true)
|
|
);
|
|
assert!(
|
|
cleaned["array"]
|
|
.as_array()
|
|
.map(|value| value.len() <= limits.max_array_items)
|
|
.unwrap_or(true)
|
|
);
|
|
assert!(!cleaned.to_string().contains("must not survive"));
|
|
assert!(cleaned.to_string().contains(TRUNCATED));
|
|
}
|
|
|
|
#[test]
|
|
fn truncation_preserves_utf8_and_never_reveals_secret_fragments() {
|
|
let input = json!({
|
|
"secret_key": "секретное-значение",
|
|
"description": "я".repeat(2048),
|
|
});
|
|
|
|
let cleaned = redact_value(&input, RedactionLimits::default());
|
|
let serialized = serde_json::to_string(&cleaned).expect("cleaned value must be valid JSON");
|
|
|
|
assert_eq!(cleaned["secret_key"], REDACTED);
|
|
assert!(!serialized.contains("секретное"));
|
|
assert!(cleaned["description"].as_str().is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn redacted_value_does_not_mutate_input() {
|
|
let input = json!({"password": "canary-secret"});
|
|
let original = input.clone();
|
|
|
|
let _ = redact_value(&input, RedactionLimits::default());
|
|
|
|
assert_eq!(input, original);
|
|
}
|
|
|
|
#[test]
|
|
fn object_keys_respect_the_string_limit() {
|
|
let limits = RedactionLimits {
|
|
max_string_bytes: 16,
|
|
..RedactionLimits::default()
|
|
};
|
|
let long_key = format!("field-{}", "x".repeat(128));
|
|
|
|
let cleaned = redact_value(&json!({ long_key: "value" }), limits);
|
|
|
|
assert!(
|
|
cleaned
|
|
.as_object()
|
|
.expect("cleaned object")
|
|
.keys()
|
|
.all(|key| key.len() <= limits.max_string_bytes)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn limits_have_finite_safe_defaults() {
|
|
let limits = RedactionLimits::default();
|
|
|
|
assert_eq!(limits.max_string_bytes, 1024);
|
|
assert_eq!(limits.max_array_items, 32);
|
|
assert_eq!(limits.max_object_fields, 64);
|
|
assert_eq!(limits.max_depth, 8);
|
|
assert_eq!(limits.max_event_bytes, 16 * 1024);
|
|
assert!(Value::Null.is_null());
|
|
}
|