mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2025-12-16 11:47:45 +01:00
formatted
This commit is contained in:
@@ -14,9 +14,7 @@ fn bench_basic_masking(c: &mut Criterion) {
|
||||
|
||||
let text = "The password is password123 and the API key is api_key_abcdef123456. Also super_secret_value_that_should_be_masked is here.";
|
||||
|
||||
c.bench_function("basic_masking", |b| {
|
||||
b.iter(|| masker.mask(black_box(text)))
|
||||
});
|
||||
c.bench_function("basic_masking", |b| b.iter(|| masker.mask(black_box(text))));
|
||||
}
|
||||
|
||||
fn bench_pattern_masking(c: &mut Criterion) {
|
||||
@@ -50,7 +48,7 @@ fn bench_large_text_masking(c: &mut Criterion) {
|
||||
|
||||
fn bench_many_secrets(c: &mut Criterion) {
|
||||
let mut masker = SecretMasker::new();
|
||||
|
||||
|
||||
// Add many secrets
|
||||
for i in 0..100 {
|
||||
masker.add_secret(format!("secret_{}", i));
|
||||
@@ -58,9 +56,7 @@ fn bench_many_secrets(c: &mut Criterion) {
|
||||
|
||||
let text = "This text contains secret_50 and secret_75 but not others.";
|
||||
|
||||
c.bench_function("many_secrets", |b| {
|
||||
b.iter(|| masker.mask(black_box(text)))
|
||||
});
|
||||
c.bench_function("many_secrets", |b| b.iter(|| masker.mask(black_box(text))));
|
||||
}
|
||||
|
||||
fn bench_contains_secrets(c: &mut Criterion) {
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
//!
|
||||
//! ```rust
|
||||
//! use wrkflw_secrets::{SecretProviderConfig, SecretManager, SecretConfig};
|
||||
//!
|
||||
//!
|
||||
//! // With prefix for better security
|
||||
//! let provider = SecretProviderConfig::Environment {
|
||||
//! prefix: Some("MYAPP_".to_string())
|
||||
@@ -190,7 +190,10 @@ mod tests {
|
||||
.expect("Failed to create manager");
|
||||
|
||||
// Use a unique test secret name to avoid conflicts
|
||||
let test_secret_name = format!("TEST_SECRET_{}", uuid::Uuid::new_v4().to_string().replace('-', "_"));
|
||||
let test_secret_name = format!(
|
||||
"TEST_SECRET_{}",
|
||||
uuid::Uuid::new_v4().to_string().replace('-', "_")
|
||||
);
|
||||
std::env::set_var(&test_secret_name, "secret_value");
|
||||
|
||||
let result = manager.get_secret(&test_secret_name).await;
|
||||
@@ -210,7 +213,10 @@ mod tests {
|
||||
.expect("Failed to create manager");
|
||||
|
||||
// Use a unique test secret name to avoid conflicts
|
||||
let test_secret_name = format!("GITHUB_TOKEN_{}", uuid::Uuid::new_v4().to_string().replace('-', "_"));
|
||||
let test_secret_name = format!(
|
||||
"GITHUB_TOKEN_{}",
|
||||
uuid::Uuid::new_v4().to_string().replace('-', "_")
|
||||
);
|
||||
std::env::set_var(&test_secret_name, "ghp_test_token");
|
||||
|
||||
let mut substitution = SecretSubstitution::new(&manager);
|
||||
|
||||
@@ -33,7 +33,7 @@ impl SecretManager {
|
||||
for (name, provider_config) in &config.providers {
|
||||
// Validate provider name
|
||||
validate_provider_name(name)?;
|
||||
|
||||
|
||||
let provider: Box<dyn SecretProvider> = match provider_config {
|
||||
SecretProviderConfig::Environment { prefix } => {
|
||||
Box::new(EnvironmentProvider::new(prefix.clone()))
|
||||
@@ -54,7 +54,7 @@ impl SecretManager {
|
||||
}
|
||||
|
||||
let rate_limiter = RateLimiter::new(config.rate_limit.clone());
|
||||
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
providers,
|
||||
|
||||
@@ -137,16 +137,28 @@ impl SecretMasker {
|
||||
let mut result = text.to_string();
|
||||
|
||||
// GitHub Personal Access Tokens
|
||||
result = patterns.github_pat.replace_all(&result, "ghp_***").to_string();
|
||||
result = patterns
|
||||
.github_pat
|
||||
.replace_all(&result, "ghp_***")
|
||||
.to_string();
|
||||
|
||||
// GitHub App tokens
|
||||
result = patterns.github_app.replace_all(&result, "ghs_***").to_string();
|
||||
result = patterns
|
||||
.github_app
|
||||
.replace_all(&result, "ghs_***")
|
||||
.to_string();
|
||||
|
||||
// GitHub OAuth tokens
|
||||
result = patterns.github_oauth.replace_all(&result, "gho_***").to_string();
|
||||
result = patterns
|
||||
.github_oauth
|
||||
.replace_all(&result, "gho_***")
|
||||
.to_string();
|
||||
|
||||
// AWS Access Key IDs
|
||||
result = patterns.aws_access_key.replace_all(&result, "AKIA***").to_string();
|
||||
result = patterns
|
||||
.aws_access_key
|
||||
.replace_all(&result, "AKIA***")
|
||||
.to_string();
|
||||
|
||||
// AWS Secret Access Keys (basic pattern)
|
||||
// Only mask if it's clearly in a secret context (basic heuristic)
|
||||
@@ -155,10 +167,16 @@ impl SecretMasker {
|
||||
}
|
||||
|
||||
// JWT tokens (basic pattern)
|
||||
result = patterns.jwt.replace_all(&result, "eyJ***.eyJ***.***").to_string();
|
||||
result = patterns
|
||||
.jwt
|
||||
.replace_all(&result, "eyJ***.eyJ***.***")
|
||||
.to_string();
|
||||
|
||||
// API keys with common prefixes
|
||||
result = patterns.api_key.replace_all(&result, "${1}=***").to_string();
|
||||
result = patterns
|
||||
.api_key
|
||||
.replace_all(&result, "${1}=***")
|
||||
.to_string();
|
||||
|
||||
result
|
||||
}
|
||||
@@ -178,12 +196,12 @@ impl SecretMasker {
|
||||
/// Check if text contains common secret patterns
|
||||
fn has_secret_patterns(&self, text: &str) -> bool {
|
||||
let patterns = PATTERNS.get_or_init(CompiledPatterns::new);
|
||||
|
||||
patterns.github_pat.is_match(text) ||
|
||||
patterns.github_app.is_match(text) ||
|
||||
patterns.github_oauth.is_match(text) ||
|
||||
patterns.aws_access_key.is_match(text) ||
|
||||
patterns.jwt.is_match(text)
|
||||
|
||||
patterns.github_pat.is_match(text)
|
||||
|| patterns.github_app.is_match(text)
|
||||
|| patterns.github_oauth.is_match(text)
|
||||
|| patterns.aws_access_key.is_match(text)
|
||||
|| patterns.jwt.is_match(text)
|
||||
}
|
||||
|
||||
/// Get the number of secrets being tracked
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{validation::validate_secret_value, SecretError, SecretProvider, SecretResult, SecretValue};
|
||||
use crate::{
|
||||
validation::validate_secret_value, SecretError, SecretProvider, SecretResult, SecretValue,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -21,7 +23,6 @@ impl Default for EnvironmentProvider {
|
||||
}
|
||||
|
||||
impl EnvironmentProvider {
|
||||
|
||||
/// Get the full environment variable name
|
||||
fn get_env_name(&self, name: &str) -> String {
|
||||
match &self.prefix {
|
||||
@@ -40,7 +41,7 @@ impl SecretProvider for EnvironmentProvider {
|
||||
Ok(value) => {
|
||||
// Validate the secret value
|
||||
validate_secret_value(&value)?;
|
||||
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("source".to_string(), "environment".to_string());
|
||||
metadata.insert("env_var".to_string(), env_name);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::{validation::validate_secret_value, SecretError, SecretProvider, SecretResult, SecretValue};
|
||||
use crate::{
|
||||
validation::validate_secret_value, SecretError, SecretProvider, SecretResult, SecretValue,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
@@ -154,7 +156,7 @@ impl SecretProvider for FileProvider {
|
||||
if let Some(value) = secrets.get(name) {
|
||||
// Validate the secret value
|
||||
validate_secret_value(value)?;
|
||||
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert("source".to_string(), "file".to_string());
|
||||
metadata.insert("file_path".to_string(), self.expand_path());
|
||||
|
||||
@@ -56,7 +56,7 @@ impl RequestTracker {
|
||||
fn cleanup_old_requests(&mut self, window_duration: Duration, now: Instant) {
|
||||
let cutoff = now - window_duration;
|
||||
self.requests.retain(|&req_time| req_time > cutoff);
|
||||
|
||||
|
||||
if let Some(&first) = self.requests.first() {
|
||||
self.first_request = first;
|
||||
}
|
||||
@@ -82,8 +82,6 @@ impl RateLimiter {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Check if a request should be allowed for the given key
|
||||
pub async fn check_rate_limit(&self, key: &str) -> SecretResult<()> {
|
||||
if !self.config.enabled {
|
||||
@@ -92,11 +90,11 @@ impl RateLimiter {
|
||||
|
||||
let now = Instant::now();
|
||||
let mut trackers = self.trackers.write().await;
|
||||
|
||||
|
||||
// Clean up old requests for existing tracker
|
||||
if let Some(tracker) = trackers.get_mut(key) {
|
||||
tracker.cleanup_old_requests(self.config.window_duration, now);
|
||||
|
||||
|
||||
// Check if we're over the limit
|
||||
if tracker.request_count() >= self.config.max_requests as usize {
|
||||
let time_until_reset = self.config.window_duration - (now - tracker.first_request);
|
||||
@@ -105,7 +103,7 @@ impl RateLimiter {
|
||||
time_until_reset.as_secs()
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
// Add the current request
|
||||
tracker.add_request(now);
|
||||
} else {
|
||||
@@ -114,7 +112,7 @@ impl RateLimiter {
|
||||
tracker.add_request(now);
|
||||
trackers.insert(key.to_string(), tracker);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -169,14 +169,17 @@ mod tests {
|
||||
// Use unique secret names to avoid test conflicts
|
||||
let github_token_name = format!("GITHUB_TOKEN_{}", std::process::id());
|
||||
let api_key_name = format!("API_KEY_{}", std::process::id());
|
||||
|
||||
|
||||
std::env::set_var(&github_token_name, "ghp_test_token");
|
||||
std::env::set_var(&api_key_name, "secret_api_key");
|
||||
|
||||
let manager = SecretManager::default().await.unwrap();
|
||||
let mut substitution = SecretSubstitution::new(&manager);
|
||||
|
||||
let input = format!("Token: ${{{{ secrets.{} }}}}, API: ${{{{ secrets.{} }}}}", github_token_name, api_key_name);
|
||||
let input = format!(
|
||||
"Token: ${{{{ secrets.{} }}}}, API: ${{{{ secrets.{} }}}}",
|
||||
github_token_name, api_key_name
|
||||
);
|
||||
let result = substitution.substitute(&input).await.unwrap();
|
||||
|
||||
assert_eq!(result, "Token: ghp_test_token, API: secret_api_key");
|
||||
|
||||
@@ -37,7 +37,8 @@ pub fn validate_secret_name(name: &str) -> SecretResult<()> {
|
||||
|
||||
if !SECRET_NAME_PATTERN.is_match(name) {
|
||||
return Err(SecretError::InvalidSecretName {
|
||||
reason: "Secret name can only contain letters, numbers, underscores, hyphens, and dots".to_string(),
|
||||
reason: "Secret name can only contain letters, numbers, underscores, hyphens, and dots"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,8 +57,8 @@ pub fn validate_secret_name(name: &str) -> SecretResult<()> {
|
||||
|
||||
// Reserved names
|
||||
let reserved_names = [
|
||||
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
|
||||
"COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
];
|
||||
|
||||
if reserved_names.contains(&name.to_uppercase().as_str()) {
|
||||
@@ -72,7 +73,7 @@ pub fn validate_secret_name(name: &str) -> SecretResult<()> {
|
||||
/// Validate a secret value
|
||||
pub fn validate_secret_value(value: &str) -> SecretResult<()> {
|
||||
let size = value.len();
|
||||
|
||||
|
||||
if size > MAX_SECRET_SIZE {
|
||||
return Err(SecretError::SecretTooLarge {
|
||||
size,
|
||||
@@ -99,12 +100,16 @@ pub fn validate_provider_name(name: &str) -> SecretResult<()> {
|
||||
}
|
||||
|
||||
if name.len() > 64 {
|
||||
return Err(SecretError::InvalidConfig(
|
||||
format!("Provider name too long: {} characters (max: 64)", name.len()),
|
||||
));
|
||||
return Err(SecretError::InvalidConfig(format!(
|
||||
"Provider name too long: {} characters (max: 64)",
|
||||
name.len()
|
||||
)));
|
||||
}
|
||||
|
||||
if !name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
|
||||
if !name
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
return Err(SecretError::InvalidConfig(
|
||||
"Provider name can only contain letters, numbers, underscores, and hyphens".to_string(),
|
||||
));
|
||||
@@ -134,19 +139,19 @@ pub fn looks_like_secret(value: &str) -> bool {
|
||||
// Check for high entropy (random-looking strings)
|
||||
let unique_chars: std::collections::HashSet<char> = value.chars().collect();
|
||||
let entropy_ratio = unique_chars.len() as f64 / value.len() as f64;
|
||||
|
||||
|
||||
if entropy_ratio > 0.6 && value.len() > 16 {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for common secret patterns
|
||||
let secret_patterns = [
|
||||
r"^[A-Za-z0-9+/=]{40,}$", // Base64-like
|
||||
r"^[a-fA-F0-9]{32,}$", // Hex strings
|
||||
r"^[A-Z0-9]{20,}$", // All caps alphanumeric
|
||||
r"^sk_[a-zA-Z0-9_-]+$", // Stripe-like keys
|
||||
r"^pk_[a-zA-Z0-9_-]+$", // Public keys
|
||||
r"^rk_[a-zA-Z0-9_-]+$", // Restricted keys
|
||||
r"^[A-Za-z0-9+/=]{40,}$", // Base64-like
|
||||
r"^[a-fA-F0-9]{32,}$", // Hex strings
|
||||
r"^[A-Z0-9]{20,}$", // All caps alphanumeric
|
||||
r"^sk_[a-zA-Z0-9_-]+$", // Stripe-like keys
|
||||
r"^pk_[a-zA-Z0-9_-]+$", // Public keys
|
||||
r"^rk_[a-zA-Z0-9_-]+$", // Restricted keys
|
||||
];
|
||||
|
||||
for pattern in &secret_patterns {
|
||||
@@ -224,7 +229,9 @@ mod tests {
|
||||
assert!(looks_like_secret("sk_test_abcdefghijklmnop1234567890"));
|
||||
assert!(looks_like_secret("abcdefghijklmnopqrstuvwxyz123456"));
|
||||
assert!(looks_like_secret("ABCDEF1234567890ABCDEF1234567890"));
|
||||
assert!(looks_like_secret("YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw"));
|
||||
assert!(looks_like_secret(
|
||||
"YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY3ODkw"
|
||||
));
|
||||
|
||||
// Should not detect as secrets
|
||||
assert!(!looks_like_secret("short"));
|
||||
|
||||
@@ -60,8 +60,14 @@ async fn test_end_to_end_secret_workflow() {
|
||||
|
||||
// Test 1: Get secret from environment provider
|
||||
let env_secret = manager.get_secret(&env_secret_name).await.unwrap();
|
||||
assert_eq!(env_secret.value(), "ghp_1234567890abcdefghijklmnopqrstuvwxyz");
|
||||
assert_eq!(env_secret.metadata.get("source"), Some(&"environment".to_string()));
|
||||
assert_eq!(
|
||||
env_secret.value(),
|
||||
"ghp_1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
);
|
||||
assert_eq!(
|
||||
env_secret.metadata.get("source"),
|
||||
Some(&"environment".to_string())
|
||||
);
|
||||
|
||||
// Test 2: Get secret from file provider
|
||||
let file_secret = manager
|
||||
@@ -69,7 +75,10 @@ async fn test_end_to_end_secret_workflow() {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(file_secret.value(), "super_secret_db_pass_123");
|
||||
assert_eq!(file_secret.metadata.get("source"), Some(&"file".to_string()));
|
||||
assert_eq!(
|
||||
file_secret.metadata.get("source"),
|
||||
Some(&"file".to_string())
|
||||
);
|
||||
|
||||
// Test 3: List secrets from file provider
|
||||
let all_secrets = manager.list_all_secrets().await.unwrap();
|
||||
@@ -152,8 +161,8 @@ async fn test_error_handling() {
|
||||
/// Test rate limiting functionality
|
||||
#[tokio::test]
|
||||
async fn test_rate_limiting() {
|
||||
use wrkflw_secrets::rate_limit::RateLimitConfig;
|
||||
use std::time::Duration;
|
||||
use wrkflw_secrets::rate_limit::RateLimitConfig;
|
||||
|
||||
// Create config with very low rate limit
|
||||
let mut config = SecretConfig::default();
|
||||
@@ -179,7 +188,10 @@ async fn test_rate_limiting() {
|
||||
// Third request should fail due to rate limiting
|
||||
let result3 = manager.get_secret(&test_secret_name).await;
|
||||
assert!(result3.is_err());
|
||||
assert!(result3.unwrap_err().to_string().contains("Rate limit exceeded"));
|
||||
assert!(result3
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Rate limit exceeded"));
|
||||
|
||||
// Cleanup
|
||||
std::env::remove_var(&test_secret_name);
|
||||
@@ -308,7 +320,10 @@ async fn test_comprehensive_masking() {
|
||||
for pattern in should_not_contain {
|
||||
if pattern != "***" {
|
||||
assert!(
|
||||
!masked.contains(pattern) || pattern == "ghp_" || pattern == "AKIA" || pattern == "eyJ",
|
||||
!masked.contains(pattern)
|
||||
|| pattern == "ghp_"
|
||||
|| pattern == "AKIA"
|
||||
|| pattern == "eyJ",
|
||||
"Masked text '{}' should not contain '{}' (or only partial patterns)",
|
||||
masked,
|
||||
pattern
|
||||
|
||||
Reference in New Issue
Block a user