feat: pentest onboarding — streaming, browser automation, reports, user cleanup (#16)
All checks were successful
CI / Check (push) Has been skipped
CI / Detect Changes (push) Successful in 7s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Successful in 2s
CI / Deploy MCP (push) Successful in 2s

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:
2026-03-17 20:32:20 +00:00
parent 11e1c5f438
commit c461faa2fb
57 changed files with 8844 additions and 2423 deletions

View File

@@ -436,6 +436,150 @@ fn pentest_event_serde_finding() {
}
}
// ─── PentestEvent Paused/Resumed ───
#[test]
fn pentest_event_serde_paused() {
let event = pentest::PentestEvent::Paused;
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"paused""#));
let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
assert!(matches!(back, pentest::PentestEvent::Paused));
}
#[test]
fn pentest_event_serde_resumed() {
let event = pentest::PentestEvent::Resumed;
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"resumed""#));
let back: pentest::PentestEvent = serde_json::from_str(&json).unwrap();
assert!(matches!(back, pentest::PentestEvent::Resumed));
}
// ─── PentestConfig serde ───
#[test]
fn pentest_config_serde_roundtrip() {
let config = pentest::PentestConfig {
app_url: "https://example.com".into(),
git_repo_url: Some("https://github.com/org/repo".into()),
branch: Some("main".into()),
commit_hash: None,
app_type: Some("web".into()),
rate_limit: Some(10),
auth: pentest::PentestAuthConfig {
mode: pentest::AuthMode::Manual,
username: Some("admin".into()),
password: Some("pass123".into()),
registration_url: None,
verification_email: None,
imap_host: None,
imap_port: None,
imap_username: None,
imap_password: None,
cleanup_test_user: true,
},
custom_headers: [("X-Token".to_string(), "abc".to_string())]
.into_iter()
.collect(),
strategy: Some("comprehensive".into()),
allow_destructive: false,
initial_instructions: Some("Test the login flow".into()),
scope_exclusions: vec!["/admin".into()],
disclaimer_accepted: true,
disclaimer_accepted_at: Some(chrono::Utc::now()),
environment: pentest::Environment::Staging,
tester: pentest::TesterInfo {
name: "Alice".into(),
email: "alice@example.com".into(),
},
max_duration_minutes: Some(30),
skip_mode: false,
};
let json = serde_json::to_string(&config).unwrap();
let back: pentest::PentestConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.app_url, "https://example.com");
assert_eq!(back.auth.mode, pentest::AuthMode::Manual);
assert_eq!(back.auth.username, Some("admin".into()));
assert!(back.auth.cleanup_test_user);
assert_eq!(back.scope_exclusions, vec!["/admin".to_string()]);
assert_eq!(back.environment, pentest::Environment::Staging);
}
#[test]
fn pentest_auth_config_default() {
let auth = pentest::PentestAuthConfig::default();
assert_eq!(auth.mode, pentest::AuthMode::None);
assert!(auth.username.is_none());
assert!(auth.password.is_none());
assert!(auth.verification_email.is_none());
assert!(auth.imap_host.is_none());
assert!(!auth.cleanup_test_user);
}
// ─── TestUserRecord ───
#[test]
fn test_user_record_default() {
let r = pentest::TestUserRecord::default();
assert!(r.username.is_none());
assert!(r.email.is_none());
assert!(r.provider_user_id.is_none());
assert!(r.provider.is_none());
assert!(!r.cleaned_up);
}
#[test]
fn test_user_record_serde_roundtrip() {
let r = pentest::TestUserRecord {
username: Some("pentestuser".into()),
email: Some("pentest+abc@scanner.example.com".into()),
provider_user_id: Some("kc-uuid-123".into()),
provider: Some(pentest::IdentityProvider::Keycloak),
cleaned_up: false,
};
let json = serde_json::to_string(&r).unwrap();
let back: pentest::TestUserRecord = serde_json::from_str(&json).unwrap();
assert_eq!(back.username, Some("pentestuser".into()));
assert_eq!(back.provider, Some(pentest::IdentityProvider::Keycloak));
assert!(!back.cleaned_up);
}
#[test]
fn identity_provider_serde_all_variants() {
for (variant, expected) in [
(pentest::IdentityProvider::Keycloak, "\"keycloak\""),
(pentest::IdentityProvider::Auth0, "\"auth0\""),
(pentest::IdentityProvider::Okta, "\"okta\""),
(pentest::IdentityProvider::Firebase, "\"firebase\""),
(pentest::IdentityProvider::Custom, "\"custom\""),
] {
let json = serde_json::to_string(&variant).unwrap();
assert_eq!(json, expected);
let back: pentest::IdentityProvider = serde_json::from_str(&json).unwrap();
assert_eq!(back, variant);
}
}
#[test]
fn pentest_session_with_test_user() {
let mut s = pentest::PentestSession::new("t".into(), pentest::PentestStrategy::Quick);
assert!(s.test_user.is_none());
s.test_user = Some(pentest::TestUserRecord {
username: Some("pentester".into()),
email: Some("pentest+123@example.com".into()),
provider_user_id: None,
provider: Some(pentest::IdentityProvider::Auth0),
cleaned_up: false,
});
let bson_doc = bson::to_document(&s).unwrap();
let back: pentest::PentestSession = bson::from_document(bson_doc).unwrap();
assert!(back.test_user.is_some());
let tu = back.test_user.as_ref().unwrap();
assert_eq!(tu.username, Some("pentester".into()));
assert_eq!(tu.provider, Some(pentest::IdentityProvider::Auth0));
}
// ─── Serde helpers (BSON datetime) ───
#[test]