1use crate::{
57 config::{
58 AuditConfig, AuditStorage, CookieSameSite, JwtAlgorithm, LockoutConfig,
59 OAuth2SecurityConfig, PasswordHashAlgorithm, RateLimitConfig, SecurityConfig,
60 },
61 prelude::{AuthFrameworkResult, hours, minutes},
62};
63use std::time::Duration;
64
65#[derive(Debug, Clone, PartialEq)]
67pub enum SecurityPreset {
68 Development,
78
79 Balanced,
89
90 HighSecurity,
102
103 Paranoid,
115}
116
117#[derive(Debug, Clone)]
119pub struct SecurityIssue {
120 pub severity: SecuritySeverity,
122 pub component: String,
124 pub description: String,
126 pub suggestion: String,
128 pub blocks_production: bool,
130}
131
132#[derive(Debug, Clone, PartialEq, PartialOrd)]
134pub enum SecuritySeverity {
135 Info,
137 Warning,
139 Error,
141 Critical,
143}
144
145impl SecurityPreset {
146 pub fn to_config(&self) -> SecurityConfig {
148 match self {
149 SecurityPreset::Development => SecurityConfig {
150 min_password_length: 6,
151 require_password_complexity: false,
152 require_uppercase: false,
153 require_lowercase: false,
154 require_digit: false,
155 require_special: false,
156 min_complexity_criteria: 0,
157 password_hash_algorithm: PasswordHashAlgorithm::Bcrypt, jwt_algorithm: JwtAlgorithm::HS256,
159 secret_key: None, secure_cookies: false, cookie_same_site: CookieSameSite::Lax,
162 csrf_protection: false, session_timeout: hours(24), lockout: LockoutConfig {
165 enabled: false, ..Default::default()
167 },
168 max_api_keys_per_user: 0, oauth2: OAuth2SecurityConfig {
170 require_user_authentication: false,
171 validate_redirect_uri: false,
172 require_client_secret: false,
173 require_pkce: false,
174 },
175 },
176 SecurityPreset::Balanced => SecurityConfig {
177 min_password_length: 8,
178 require_password_complexity: true,
179 require_uppercase: true,
180 require_lowercase: true,
181 require_digit: true,
182 require_special: false, min_complexity_criteria: 3,
184 password_hash_algorithm: PasswordHashAlgorithm::Argon2,
185 jwt_algorithm: JwtAlgorithm::HS256,
186 secret_key: None,
187 secure_cookies: true,
188 cookie_same_site: CookieSameSite::Lax,
189 csrf_protection: true,
190 session_timeout: hours(8),
191 lockout: LockoutConfig {
192 enabled: true,
193 max_failed_attempts: 5,
194 lockout_duration_seconds: 900, progressive_lockout: false,
196 ..Default::default()
197 },
198 max_api_keys_per_user: 10,
199 oauth2: OAuth2SecurityConfig {
200 require_user_authentication: true,
201 validate_redirect_uri: true,
202 require_client_secret: true,
203 require_pkce: false, },
205 },
206 SecurityPreset::HighSecurity => SecurityConfig {
207 min_password_length: 12,
208 require_password_complexity: true,
209 require_uppercase: true,
210 require_lowercase: true,
211 require_digit: true,
212 require_special: true,
213 min_complexity_criteria: 4, password_hash_algorithm: PasswordHashAlgorithm::Argon2,
215 jwt_algorithm: JwtAlgorithm::RS256, secret_key: None,
217 secure_cookies: true,
218 cookie_same_site: CookieSameSite::Strict,
219 csrf_protection: true,
220 session_timeout: hours(2), lockout: LockoutConfig {
222 enabled: true,
223 max_failed_attempts: 3, lockout_duration_seconds: 1800, progressive_lockout: true,
226 ..Default::default()
227 },
228 max_api_keys_per_user: 5, oauth2: OAuth2SecurityConfig::default(), },
231 SecurityPreset::Paranoid => SecurityConfig {
232 min_password_length: 16,
233 require_password_complexity: true,
234 require_uppercase: true,
235 require_lowercase: true,
236 require_digit: true,
237 require_special: true,
238 min_complexity_criteria: 4, password_hash_algorithm: PasswordHashAlgorithm::Argon2,
240 jwt_algorithm: JwtAlgorithm::RS512, secret_key: None,
242 secure_cookies: true,
243 cookie_same_site: CookieSameSite::Strict,
244 csrf_protection: true,
245 session_timeout: minutes(30), lockout: LockoutConfig {
247 enabled: true,
248 max_failed_attempts: 3,
249 lockout_duration_seconds: 3600, progressive_lockout: true,
251 max_lockout_duration_seconds: 86400, ..Default::default()
253 },
254 max_api_keys_per_user: 3, oauth2: OAuth2SecurityConfig::default(), },
257 }
258 }
259
260 pub fn to_rate_limit_config(&self) -> RateLimitConfig {
262 match self {
263 SecurityPreset::Development => RateLimitConfig {
264 enabled: false, max_requests: 1000,
266 window: Duration::from_secs(60),
267 burst: 100,
268 per_user_enabled: false,
269 max_requests_per_user: 0,
270 per_user_window: Duration::from_secs(60),
271 },
272 SecurityPreset::Balanced => RateLimitConfig {
273 enabled: true,
274 max_requests: 100,
275 window: Duration::from_secs(60),
276 burst: 20,
277 per_user_enabled: true,
278 max_requests_per_user: 120, per_user_window: Duration::from_secs(60),
280 },
281 SecurityPreset::HighSecurity => RateLimitConfig {
282 enabled: true,
283 max_requests: 60, window: Duration::from_secs(60),
285 burst: 10,
286 per_user_enabled: true,
287 max_requests_per_user: 60,
288 per_user_window: Duration::from_secs(60),
289 },
290 SecurityPreset::Paranoid => RateLimitConfig {
291 enabled: true,
292 max_requests: 30, window: Duration::from_secs(60),
294 burst: 5,
295 per_user_enabled: true,
296 max_requests_per_user: 30,
297 per_user_window: Duration::from_secs(60),
298 },
299 }
300 }
301
302 pub fn to_audit_config(&self) -> AuditConfig {
304 match self {
305 SecurityPreset::Development => AuditConfig {
306 enabled: false, log_success: false,
308 log_failures: true, log_permissions: false,
310 log_tokens: false,
311 storage: AuditStorage::Tracing,
312 },
313 SecurityPreset::Balanced => AuditConfig {
314 enabled: true,
315 log_success: false, log_failures: true,
317 log_permissions: true,
318 log_tokens: false, storage: AuditStorage::Tracing,
320 },
321 SecurityPreset::HighSecurity => AuditConfig {
322 enabled: true,
323 log_success: true,
324 log_failures: true,
325 log_permissions: true,
326 log_tokens: false,
327 storage: AuditStorage::Tracing, },
329 SecurityPreset::Paranoid => AuditConfig {
330 enabled: true,
331 log_success: true,
332 log_failures: true,
333 log_permissions: true,
334 log_tokens: true, storage: AuditStorage::Tracing, },
337 }
338 }
339
340 pub async fn validate_environment(&self) -> AuthFrameworkResult<Vec<SecurityIssue>> {
342 let mut issues = Vec::new();
343
344 let is_production = self.is_production_environment();
346 let is_development = self.is_development_environment();
347
348 match (self, is_production, is_development) {
350 (SecurityPreset::Development, true, false) => {
351 issues.push(SecurityIssue {
352 severity: SecuritySeverity::Critical,
353 component: "Security Preset".to_string(),
354 description: "Development security preset used in production environment".to_string(),
355 suggestion: "Use SecurityPreset::HighSecurity or SecurityPreset::Paranoid for production".to_string(),
356 blocks_production: true,
357 });
358 }
359 (SecurityPreset::Balanced, true, false) => {
360 issues.push(SecurityIssue {
361 severity: SecuritySeverity::Warning,
362 component: "Security Preset".to_string(),
363 description:
364 "Balanced security preset in production - consider higher security"
365 .to_string(),
366 suggestion: "Consider SecurityPreset::HighSecurity for better protection"
367 .to_string(),
368 blocks_production: false,
369 });
370 }
371 _ => {} }
373
374 self.validate_jwt_secret(&mut issues);
376
377 if is_production && self.requires_secure_cookies() {
379 self.validate_https_requirement(&mut issues);
380 }
381
382 self.validate_storage_security(&mut issues);
384
385 self.validate_environment_variables(&mut issues);
387
388 Ok(issues)
389 }
390
391 pub async fn security_audit(&self) -> AuthFrameworkResult<SecurityAuditReport> {
393 let issues = self.validate_environment().await?;
394
395 let critical_count = issues
396 .iter()
397 .filter(|i| i.severity == SecuritySeverity::Critical)
398 .count();
399 let error_count = issues
400 .iter()
401 .filter(|i| i.severity == SecuritySeverity::Error)
402 .count();
403 let warning_count = issues
404 .iter()
405 .filter(|i| i.severity == SecuritySeverity::Warning)
406 .count();
407
408 let overall_status = if critical_count > 0 {
409 SecurityAuditStatus::Critical
410 } else if error_count > 0 {
411 SecurityAuditStatus::Failed
412 } else if warning_count > 0 {
413 SecurityAuditStatus::Warning
414 } else {
415 SecurityAuditStatus::Passed
416 };
417
418 Ok(SecurityAuditReport {
419 preset: self.clone(),
420 status: overall_status,
421 issues,
422 critical_count,
423 error_count,
424 warning_count,
425 recommendations: self.get_security_recommendations(),
426 })
427 }
428
429 pub fn get_security_recommendations(&self) -> Vec<String> {
431 let mut recommendations = Vec::new();
432
433 match self {
434 SecurityPreset::Development => {
435 recommendations.push(
436 "â ī¸ Development preset detected - ensure this is not used in production"
437 .to_string(),
438 );
439 recommendations
440 .push("đ Set JWT_SECRET environment variable with a secure value".to_string());
441 recommendations
442 .push("đ Enable audit logging when moving to production".to_string());
443 }
444 SecurityPreset::Balanced => {
445 recommendations.push(
446 "đ Consider upgrading to HighSecurity for sensitive applications".to_string(),
447 );
448 recommendations
449 .push("đ Monitor authentication patterns for suspicious activity".to_string());
450 recommendations.push("đ Regularly rotate JWT secrets and API keys".to_string());
451 }
452 SecurityPreset::HighSecurity => {
453 recommendations
454 .push("â
Good security configuration for production use".to_string());
455 recommendations
456 .push("đ Ensure RSA keys are properly managed and rotated".to_string());
457 recommendations.push("đ Monitor failed authentication attempts".to_string());
458 recommendations.push(
459 "đĄī¸ Consider multi-factor authentication for admin accounts".to_string(),
460 );
461 }
462 SecurityPreset::Paranoid => {
463 recommendations
464 .push("đĄī¸ Maximum security enabled - monitor performance impact".to_string());
465 recommendations.push(
466 "⥠Consider connection pooling to handle strict rate limits".to_string(),
467 );
468 recommendations.push("đ Implement comprehensive security monitoring".to_string());
469 recommendations
470 .push("đ¨ Set up alerting for all authentication failures".to_string());
471 }
472 }
473
474 recommendations
475 }
476
477 fn is_production_environment(&self) -> bool {
480 std::env::var("ENVIRONMENT").as_deref() == Ok("production")
481 || std::env::var("ENV").as_deref() == Ok("production")
482 || std::env::var("NODE_ENV").as_deref() == Ok("production")
483 || std::env::var("RUST_ENV").as_deref() == Ok("production")
484 || std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
485 }
486
487 fn is_development_environment(&self) -> bool {
488 std::env::var("ENVIRONMENT").as_deref() == Ok("development")
489 || std::env::var("ENV").as_deref() == Ok("development")
490 || std::env::var("NODE_ENV").as_deref() == Ok("development")
491 || std::env::var("RUST_ENV").as_deref() == Ok("development")
492 || cfg!(debug_assertions)
493 }
494
495 fn requires_secure_cookies(&self) -> bool {
496 matches!(
497 self,
498 SecurityPreset::Balanced | SecurityPreset::HighSecurity | SecurityPreset::Paranoid
499 )
500 }
501
502 fn validate_jwt_secret(&self, issues: &mut Vec<SecurityIssue>) {
503 if let Ok(secret) = std::env::var("JWT_SECRET") {
504 let min_length = match self {
505 SecurityPreset::Development => 16,
506 SecurityPreset::Balanced => 32,
507 SecurityPreset::HighSecurity => 64,
508 SecurityPreset::Paranoid => 128,
509 };
510
511 if secret.len() < min_length {
512 issues.push(SecurityIssue {
513 severity: if matches!(self, SecurityPreset::Development) {
514 SecuritySeverity::Warning
515 } else {
516 SecuritySeverity::Error
517 },
518 component: "JWT Secret".to_string(),
519 description: format!(
520 "JWT secret too short ({} chars, need {}+)",
521 secret.len(),
522 min_length
523 ),
524 suggestion: format!(
525 "Generate a longer secret: `openssl rand -base64 {}`",
526 min_length * 3 / 4
527 ),
528 blocks_production: !matches!(self, SecurityPreset::Development),
529 });
530 }
531
532 if secret.to_lowercase().contains("secret")
534 || secret.to_lowercase().contains("password")
535 || secret.contains("123")
536 {
537 issues.push(SecurityIssue {
538 severity: SecuritySeverity::Error,
539 component: "JWT Secret".to_string(),
540 description: "JWT secret contains weak patterns or common words".to_string(),
541 suggestion:
542 "Use a cryptographically secure random string: `openssl rand -base64 64`"
543 .to_string(),
544 blocks_production: true,
545 });
546 }
547 } else {
548 issues.push(SecurityIssue {
549 severity: SecuritySeverity::Critical,
550 component: "JWT Secret".to_string(),
551 description: "JWT_SECRET environment variable not set".to_string(),
552 suggestion: "Set JWT_SECRET environment variable with a secure random value"
553 .to_string(),
554 blocks_production: true,
555 });
556 }
557 }
558
559 fn validate_https_requirement(&self, issues: &mut Vec<SecurityIssue>) {
560 let has_tls_cert =
563 std::env::var("TLS_CERT_PATH").is_ok() || std::env::var("SSL_CERT_PATH").is_ok();
564 let behind_proxy = std::env::var("HTTPS").as_deref() == Ok("on")
565 || std::env::var("HTTP_X_FORWARDED_PROTO").as_deref() == Ok("https");
566
567 if !has_tls_cert && !behind_proxy {
568 issues.push(SecurityIssue {
569 severity: SecuritySeverity::Warning,
570 component: "HTTPS".to_string(),
571 description: "HTTPS configuration not detected".to_string(),
572 suggestion: "Ensure HTTPS is properly configured for secure cookie transmission"
573 .to_string(),
574 blocks_production: false,
575 });
576 }
577 }
578
579 fn validate_storage_security(&self, issues: &mut Vec<SecurityIssue>) {
580 if let Ok(db_url) = std::env::var("DATABASE_URL")
582 && db_url.starts_with("postgresql://")
583 && !db_url.contains("sslmode=require")
584 {
585 issues.push(SecurityIssue {
586 severity: SecuritySeverity::Warning,
587 component: "Database".to_string(),
588 description: "Database connection may not be using SSL".to_string(),
589 suggestion: "Add sslmode=require to DATABASE_URL for encrypted connections"
590 .to_string(),
591 blocks_production: false,
592 });
593 }
594
595 if let Ok(redis_url) = std::env::var("REDIS_URL")
596 && !redis_url.starts_with("rediss://")
597 && !redis_url.contains("tls")
598 {
599 issues.push(SecurityIssue {
600 severity: SecuritySeverity::Info,
601 component: "Redis".to_string(),
602 description: "Redis connection may not be using TLS".to_string(),
603 suggestion: "Consider using rediss:// URL or enabling TLS for Redis connections"
604 .to_string(),
605 blocks_production: false,
606 });
607 }
608 }
609
610 fn validate_environment_variables(&self, issues: &mut Vec<SecurityIssue>) {
611 let sensitive_vars = [
612 "JWT_SECRET",
613 "DATABASE_URL",
614 "REDIS_URL",
615 "OAUTH_CLIENT_SECRET",
616 ];
617
618 for var in &sensitive_vars {
619 if let Ok(value) = std::env::var(var)
620 && value.len() < 20
621 {
622 issues.push(SecurityIssue {
623 severity: SecuritySeverity::Warning,
624 component: "Environment Variables".to_string(),
625 description: format!("{} appears to be too short", var),
626 suggestion: format!(
627 "Ensure {} contains a sufficiently long, secure value",
628 var
629 ),
630 blocks_production: false,
631 });
632 }
633 }
634 }
635}
636
637#[derive(Debug, Clone)]
639pub struct SecurityAuditReport {
640 pub preset: SecurityPreset,
641 pub status: SecurityAuditStatus,
642 pub issues: Vec<SecurityIssue>,
643 pub critical_count: usize,
644 pub error_count: usize,
645 pub warning_count: usize,
646 pub recommendations: Vec<String>,
647}
648
649#[derive(Debug, Clone, PartialEq)]
651pub enum SecurityAuditStatus {
652 Passed,
654 Warning,
656 Failed,
658 Critical,
660}
661
662impl SecurityAuditReport {
663 pub fn print_report(&self) {
665 println!("đ Security Audit Report");
666 println!("========================");
667 println!("Preset: {:?}", self.preset);
668 println!("Status: {}", self.status_emoji());
669 println!();
670
671 if self.issues.is_empty() {
672 println!("â
No security issues found!");
673 } else {
674 println!("đ Issues Summary:");
675 println!(" Critical: {}", self.critical_count);
676 println!(" Errors: {}", self.error_count);
677 println!(" Warnings: {}", self.warning_count);
678 println!();
679
680 for issue in &self.issues {
681 println!(
682 "{} {}: {}",
683 issue.severity.emoji(),
684 issue.component,
685 issue.description
686 );
687 println!(" đĄ {}", issue.suggestion);
688 println!();
689 }
690 }
691
692 if !self.recommendations.is_empty() {
693 println!("đ Recommendations:");
694 for rec in &self.recommendations {
695 println!(" {}", rec);
696 }
697 println!();
698 }
699
700 println!("{}", self.get_summary_message());
701 }
702
703 fn status_emoji(&self) -> &str {
704 match self.status {
705 SecurityAuditStatus::Passed => "â
Passed",
706 SecurityAuditStatus::Warning => "â ī¸ Warning",
707 SecurityAuditStatus::Failed => "â Failed",
708 SecurityAuditStatus::Critical => "đ¨ Critical",
709 }
710 }
711
712 fn get_summary_message(&self) -> String {
713 match self.status {
714 SecurityAuditStatus::Passed => "đ Security audit passed! Your configuration meets security requirements.".to_string(),
715 SecurityAuditStatus::Warning => "â ī¸ Security audit completed with warnings. Consider addressing the issues above.".to_string(),
716 SecurityAuditStatus::Failed => "â Security audit failed. Please address the errors before deploying to production.".to_string(),
717 SecurityAuditStatus::Critical => "đ¨ Critical security issues found! Do not deploy to production until these are resolved.".to_string(),
718 }
719 }
720
721 pub fn is_production_ready(&self) -> bool {
723 matches!(
724 self.status,
725 SecurityAuditStatus::Passed | SecurityAuditStatus::Warning
726 )
727 }
728
729 pub fn get_blocking_issues(&self) -> Vec<&SecurityIssue> {
731 self.issues
732 .iter()
733 .filter(|issue| issue.blocks_production)
734 .collect()
735 }
736}
737
738impl SecuritySeverity {
739 fn emoji(&self) -> &str {
740 match self {
741 SecuritySeverity::Info => "âšī¸ ",
742 SecuritySeverity::Warning => "â ī¸ ",
743 SecuritySeverity::Error => "â",
744 SecuritySeverity::Critical => "đ¨",
745 }
746 }
747}
748
749impl std::fmt::Display for SecuritySeverity {
750 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
751 match self {
752 SecuritySeverity::Info => write!(f, "INFO"),
753 SecuritySeverity::Warning => write!(f, "WARNING"),
754 SecuritySeverity::Error => write!(f, "ERROR"),
755 SecuritySeverity::Critical => write!(f, "CRITICAL"),
756 }
757 }
758}