feat: pentest onboarding — streaming, browser automation, reports, user cleanup (#16)
All checks were successful
All checks were successful
Complete pentest feature overhaul: SSE streaming, session-persistent browser tool (CDP), AES-256 credential encryption, auto-screenshots in reports, code-level remediation correlation, SAST triage chunking, context window optimization, test user cleanup (Keycloak/Auth0/Okta), wizard dropdowns, attack chain improvements, architecture docs with Mermaid diagrams. Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Reviewed-on: #16
This commit was merged in pull request #16.
This commit is contained in:
117
compliance-agent/src/pentest/crypto.rs
Normal file
117
compliance-agent/src/pentest/crypto.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use aes_gcm::aead::AeadCore;
|
||||
use aes_gcm::{
|
||||
aead::{Aead, KeyInit, OsRng},
|
||||
Aes256Gcm, Nonce,
|
||||
};
|
||||
use base64::Engine;
|
||||
|
||||
/// Load the 32-byte encryption key from PENTEST_ENCRYPTION_KEY env var.
|
||||
/// Returns None if not set or invalid length.
|
||||
pub fn load_encryption_key() -> Option<[u8; 32]> {
|
||||
let hex_key = std::env::var("PENTEST_ENCRYPTION_KEY").ok()?;
|
||||
let bytes = hex::decode(hex_key).ok()?;
|
||||
if bytes.len() != 32 {
|
||||
return None;
|
||||
}
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&bytes);
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// Encrypt a plaintext string. Returns base64-encoded nonce+ciphertext.
|
||||
/// Returns the original string if no encryption key is available.
|
||||
pub fn encrypt(plaintext: &str) -> String {
|
||||
let Some(key_bytes) = load_encryption_key() else {
|
||||
return plaintext.to_string();
|
||||
};
|
||||
let Ok(cipher) = Aes256Gcm::new_from_slice(&key_bytes) else {
|
||||
return plaintext.to_string();
|
||||
};
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let Ok(ciphertext) = cipher.encrypt(&nonce, plaintext.as_bytes()) else {
|
||||
return plaintext.to_string();
|
||||
};
|
||||
let mut combined = nonce.to_vec();
|
||||
combined.extend_from_slice(&ciphertext);
|
||||
base64::engine::general_purpose::STANDARD.encode(&combined)
|
||||
}
|
||||
|
||||
/// Decrypt a base64-encoded nonce+ciphertext string.
|
||||
/// Returns None if decryption fails.
|
||||
pub fn decrypt(encrypted: &str) -> Option<String> {
|
||||
let key_bytes = load_encryption_key()?;
|
||||
let cipher = Aes256Gcm::new_from_slice(&key_bytes).ok()?;
|
||||
let combined = base64::engine::general_purpose::STANDARD
|
||||
.decode(encrypted)
|
||||
.ok()?;
|
||||
if combined.len() < 12 {
|
||||
return None;
|
||||
}
|
||||
let (nonce_bytes, ciphertext) = combined.split_at(12);
|
||||
let nonce = Nonce::from_slice(nonce_bytes);
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext).ok()?;
|
||||
String::from_utf8(plaintext).ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Guard to serialize tests that touch env vars
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn with_key<F: FnOnce()>(hex_key: &str, f: F) {
|
||||
let _guard = ENV_LOCK.lock();
|
||||
unsafe { std::env::set_var("PENTEST_ENCRYPTION_KEY", hex_key) };
|
||||
f();
|
||||
unsafe { std::env::remove_var("PENTEST_ENCRYPTION_KEY") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
with_key(key, || {
|
||||
let plaintext = "my_secret_password";
|
||||
let encrypted = encrypt(plaintext);
|
||||
assert_ne!(encrypted, plaintext);
|
||||
let decrypted = decrypt(&encrypted);
|
||||
assert_eq!(decrypted, Some(plaintext.to_string()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_key_fails() {
|
||||
let _guard = ENV_LOCK.lock();
|
||||
let key1 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
let key2 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
||||
let encrypted = {
|
||||
unsafe { std::env::set_var("PENTEST_ENCRYPTION_KEY", key1) };
|
||||
let e = encrypt("secret");
|
||||
unsafe { std::env::remove_var("PENTEST_ENCRYPTION_KEY") };
|
||||
e
|
||||
};
|
||||
unsafe { std::env::set_var("PENTEST_ENCRYPTION_KEY", key2) };
|
||||
assert!(decrypt(&encrypted).is_none());
|
||||
unsafe { std::env::remove_var("PENTEST_ENCRYPTION_KEY") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_key_passthrough() {
|
||||
let _guard = ENV_LOCK.lock();
|
||||
unsafe { std::env::remove_var("PENTEST_ENCRYPTION_KEY") };
|
||||
let result = encrypt("plain");
|
||||
assert_eq!(result, "plain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupted_ciphertext() {
|
||||
let key = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
with_key(key, || {
|
||||
assert!(decrypt("not-valid-base64!!!").is_none());
|
||||
// Valid base64 but wrong content
|
||||
let garbage = base64::engine::general_purpose::STANDARD.encode(b"tooshort");
|
||||
assert!(decrypt(&garbage).is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user