1use crate::{
57 config::{
58 AuditConfig, AuditStorage, CookieSameSite, JwtAlgorithm, PasswordHashAlgorithm,
59 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 password_hash_algorithm: PasswordHashAlgorithm::Bcrypt, jwt_algorithm: JwtAlgorithm::HS256,
154 secret_key: None, previous_secret_key: None,
156 secure_cookies: false, cookie_same_site: CookieSameSite::Lax,
158 csrf_protection: false, session_timeout: hours(24), },
161 SecurityPreset::Balanced => SecurityConfig {
162 min_password_length: 8,
163 require_password_complexity: true,
164 password_hash_algorithm: PasswordHashAlgorithm::Argon2,
165 jwt_algorithm: JwtAlgorithm::HS256,
166 secret_key: None,
167 previous_secret_key: None,
168 secure_cookies: true,
169 cookie_same_site: CookieSameSite::Lax,
170 csrf_protection: true,
171 session_timeout: hours(8),
172 },
173 SecurityPreset::HighSecurity => SecurityConfig {
174 min_password_length: 12,
175 require_password_complexity: true,
176 password_hash_algorithm: PasswordHashAlgorithm::Argon2,
177 jwt_algorithm: JwtAlgorithm::RS256, secret_key: None,
179 previous_secret_key: None,
180 secure_cookies: true,
181 cookie_same_site: CookieSameSite::Strict,
182 csrf_protection: true,
183 session_timeout: hours(2), },
185 SecurityPreset::Paranoid => SecurityConfig {
186 min_password_length: 16,
187 require_password_complexity: true,
188 password_hash_algorithm: PasswordHashAlgorithm::Argon2,
189 jwt_algorithm: JwtAlgorithm::RS512, secret_key: None,
191 previous_secret_key: None,
192 secure_cookies: true,
193 cookie_same_site: CookieSameSite::Strict,
194 csrf_protection: true,
195 session_timeout: minutes(30), },
197 }
198 }
199
200 pub fn to_rate_limit_config(&self) -> RateLimitConfig {
202 match self {
203 SecurityPreset::Development => RateLimitConfig {
204 enabled: false, max_requests: 1000,
206 window: Duration::from_secs(60),
207 burst: 100,
208 },
209 SecurityPreset::Balanced => RateLimitConfig {
210 enabled: true,
211 max_requests: 100,
212 window: Duration::from_secs(60),
213 burst: 20,
214 },
215 SecurityPreset::HighSecurity => RateLimitConfig {
216 enabled: true,
217 max_requests: 60, window: Duration::from_secs(60),
219 burst: 10,
220 },
221 SecurityPreset::Paranoid => RateLimitConfig {
222 enabled: true,
223 max_requests: 30, window: Duration::from_secs(60),
225 burst: 5,
226 },
227 }
228 }
229
230 pub fn to_audit_config(&self) -> AuditConfig {
232 match self {
233 SecurityPreset::Development => AuditConfig {
234 enabled: false, log_success: false,
236 log_failures: true, log_permissions: false,
238 log_tokens: false,
239 storage: AuditStorage::Tracing,
240 },
241 SecurityPreset::Balanced => AuditConfig {
242 enabled: true,
243 log_success: false, log_failures: true,
245 log_permissions: true,
246 log_tokens: false, storage: AuditStorage::Tracing,
248 },
249 SecurityPreset::HighSecurity => AuditConfig {
250 enabled: true,
251 log_success: true,
252 log_failures: true,
253 log_permissions: true,
254 log_tokens: false,
255 storage: AuditStorage::Tracing, },
257 SecurityPreset::Paranoid => AuditConfig {
258 enabled: true,
259 log_success: true,
260 log_failures: true,
261 log_permissions: true,
262 log_tokens: true, storage: AuditStorage::Tracing, },
265 }
266 }
267
268 pub async fn validate_environment(&self) -> AuthFrameworkResult<Vec<SecurityIssue>> {
270 let mut issues = Vec::new();
271
272 let is_production = self.is_production_environment();
274 let is_development = self.is_development_environment();
275
276 match (self, is_production, is_development) {
278 (SecurityPreset::Development, true, false) => {
279 issues.push(SecurityIssue {
280 severity: SecuritySeverity::Critical,
281 component: "Security Preset".to_string(),
282 description: "Development security preset used in production environment".to_string(),
283 suggestion: "Use SecurityPreset::HighSecurity or SecurityPreset::Paranoid for production".to_string(),
284 blocks_production: true,
285 });
286 }
287 (SecurityPreset::Balanced, true, false) => {
288 issues.push(SecurityIssue {
289 severity: SecuritySeverity::Warning,
290 component: "Security Preset".to_string(),
291 description:
292 "Balanced security preset in production - consider higher security"
293 .to_string(),
294 suggestion: "Consider SecurityPreset::HighSecurity for better protection"
295 .to_string(),
296 blocks_production: false,
297 });
298 }
299 _ => {} }
301
302 self.validate_jwt_secret(&mut issues);
304
305 if is_production && self.requires_secure_cookies() {
307 self.validate_https_requirement(&mut issues);
308 }
309
310 self.validate_storage_security(&mut issues);
312
313 self.validate_environment_variables(&mut issues);
315
316 Ok(issues)
317 }
318
319 pub async fn security_audit(&self) -> AuthFrameworkResult<SecurityAuditReport> {
321 let issues = self.validate_environment().await?;
322
323 let critical_count = issues
324 .iter()
325 .filter(|i| i.severity == SecuritySeverity::Critical)
326 .count();
327 let error_count = issues
328 .iter()
329 .filter(|i| i.severity == SecuritySeverity::Error)
330 .count();
331 let warning_count = issues
332 .iter()
333 .filter(|i| i.severity == SecuritySeverity::Warning)
334 .count();
335
336 let overall_status = if critical_count > 0 {
337 SecurityAuditStatus::Critical
338 } else if error_count > 0 {
339 SecurityAuditStatus::Failed
340 } else if warning_count > 0 {
341 SecurityAuditStatus::Warning
342 } else {
343 SecurityAuditStatus::Passed
344 };
345
346 Ok(SecurityAuditReport {
347 preset: self.clone(),
348 status: overall_status,
349 issues,
350 critical_count,
351 error_count,
352 warning_count,
353 recommendations: self.get_security_recommendations(),
354 })
355 }
356
357 pub fn get_security_recommendations(&self) -> Vec<String> {
359 let mut recommendations = Vec::new();
360
361 match self {
362 SecurityPreset::Development => {
363 recommendations.push(
364 "â ī¸ Development preset detected - ensure this is not used in production"
365 .to_string(),
366 );
367 recommendations
368 .push("đ Set JWT_SECRET environment variable with a secure value".to_string());
369 recommendations
370 .push("đ Enable audit logging when moving to production".to_string());
371 }
372 SecurityPreset::Balanced => {
373 recommendations.push(
374 "đ Consider upgrading to HighSecurity for sensitive applications".to_string(),
375 );
376 recommendations
377 .push("đ Monitor authentication patterns for suspicious activity".to_string());
378 recommendations.push("đ Regularly rotate JWT secrets and API keys".to_string());
379 }
380 SecurityPreset::HighSecurity => {
381 recommendations
382 .push("â
Good security configuration for production use".to_string());
383 recommendations
384 .push("đ Ensure RSA keys are properly managed and rotated".to_string());
385 recommendations.push("đ Monitor failed authentication attempts".to_string());
386 recommendations.push(
387 "đĄī¸ Consider multi-factor authentication for admin accounts".to_string(),
388 );
389 }
390 SecurityPreset::Paranoid => {
391 recommendations
392 .push("đĄī¸ Maximum security enabled - monitor performance impact".to_string());
393 recommendations.push(
394 "⥠Consider connection pooling to handle strict rate limits".to_string(),
395 );
396 recommendations.push("đ Implement comprehensive security monitoring".to_string());
397 recommendations
398 .push("đ¨ Set up alerting for all authentication failures".to_string());
399 }
400 }
401
402 recommendations
403 }
404
405 fn is_production_environment(&self) -> bool {
408 std::env::var("ENVIRONMENT").as_deref() == Ok("production")
409 || std::env::var("ENV").as_deref() == Ok("production")
410 || std::env::var("NODE_ENV").as_deref() == Ok("production")
411 || std::env::var("RUST_ENV").as_deref() == Ok("production")
412 || std::env::var("KUBERNETES_SERVICE_HOST").is_ok()
413 }
414
415 fn is_development_environment(&self) -> bool {
416 std::env::var("ENVIRONMENT").as_deref() == Ok("development")
417 || std::env::var("ENV").as_deref() == Ok("development")
418 || std::env::var("NODE_ENV").as_deref() == Ok("development")
419 || std::env::var("RUST_ENV").as_deref() == Ok("development")
420 || cfg!(debug_assertions)
421 }
422
423 fn requires_secure_cookies(&self) -> bool {
424 matches!(
425 self,
426 SecurityPreset::Balanced | SecurityPreset::HighSecurity | SecurityPreset::Paranoid
427 )
428 }
429
430 fn validate_jwt_secret(&self, issues: &mut Vec<SecurityIssue>) {
431 if let Ok(secret) = std::env::var("JWT_SECRET") {
432 let min_length = match self {
433 SecurityPreset::Development => 16,
434 SecurityPreset::Balanced => 32,
435 SecurityPreset::HighSecurity => 64,
436 SecurityPreset::Paranoid => 128,
437 };
438
439 if secret.len() < min_length {
440 issues.push(SecurityIssue {
441 severity: if matches!(self, SecurityPreset::Development) {
442 SecuritySeverity::Warning
443 } else {
444 SecuritySeverity::Error
445 },
446 component: "JWT Secret".to_string(),
447 description: format!(
448 "JWT secret too short ({} chars, need {}+)",
449 secret.len(),
450 min_length
451 ),
452 suggestion: format!(
453 "Generate a longer secret: `openssl rand -base64 {}`",
454 min_length * 3 / 4
455 ),
456 blocks_production: !matches!(self, SecurityPreset::Development),
457 });
458 }
459
460 if secret.to_lowercase().contains("secret")
462 || secret.to_lowercase().contains("password")
463 || secret.contains("123")
464 {
465 issues.push(SecurityIssue {
466 severity: SecuritySeverity::Error,
467 component: "JWT Secret".to_string(),
468 description: "JWT secret contains weak patterns or common words".to_string(),
469 suggestion:
470 "Use a cryptographically secure random string: `openssl rand -base64 64`"
471 .to_string(),
472 blocks_production: true,
473 });
474 }
475 } else {
476 issues.push(SecurityIssue {
477 severity: SecuritySeverity::Critical,
478 component: "JWT Secret".to_string(),
479 description: "JWT_SECRET environment variable not set".to_string(),
480 suggestion: "Set JWT_SECRET environment variable with a secure random value"
481 .to_string(),
482 blocks_production: true,
483 });
484 }
485 }
486
487 fn validate_https_requirement(&self, issues: &mut Vec<SecurityIssue>) {
488 let has_tls_cert =
491 std::env::var("TLS_CERT_PATH").is_ok() || std::env::var("SSL_CERT_PATH").is_ok();
492 let behind_proxy = std::env::var("HTTPS").as_deref() == Ok("on")
493 || std::env::var("HTTP_X_FORWARDED_PROTO").as_deref() == Ok("https");
494
495 if !has_tls_cert && !behind_proxy {
496 issues.push(SecurityIssue {
497 severity: SecuritySeverity::Warning,
498 component: "HTTPS".to_string(),
499 description: "HTTPS configuration not detected".to_string(),
500 suggestion: "Ensure HTTPS is properly configured for secure cookie transmission"
501 .to_string(),
502 blocks_production: false,
503 });
504 }
505 }
506
507 fn validate_storage_security(&self, issues: &mut Vec<SecurityIssue>) {
508 if let Ok(db_url) = std::env::var("DATABASE_URL")
510 && db_url.starts_with("postgresql://")
511 && !db_url.contains("sslmode=require")
512 {
513 issues.push(SecurityIssue {
514 severity: SecuritySeverity::Warning,
515 component: "Database".to_string(),
516 description: "Database connection may not be using SSL".to_string(),
517 suggestion: "Add sslmode=require to DATABASE_URL for encrypted connections"
518 .to_string(),
519 blocks_production: false,
520 });
521 }
522
523 if let Ok(redis_url) = std::env::var("REDIS_URL")
524 && !redis_url.starts_with("rediss://")
525 && !redis_url.contains("tls")
526 {
527 issues.push(SecurityIssue {
528 severity: SecuritySeverity::Info,
529 component: "Redis".to_string(),
530 description: "Redis connection may not be using TLS".to_string(),
531 suggestion: "Consider using rediss:// URL or enabling TLS for Redis connections"
532 .to_string(),
533 blocks_production: false,
534 });
535 }
536 }
537
538 fn validate_environment_variables(&self, issues: &mut Vec<SecurityIssue>) {
539 let sensitive_vars = [
540 "JWT_SECRET",
541 "DATABASE_URL",
542 "REDIS_URL",
543 "OAUTH_CLIENT_SECRET",
544 ];
545
546 for var in &sensitive_vars {
547 if let Ok(value) = std::env::var(var)
548 && value.len() < 20
549 {
550 issues.push(SecurityIssue {
551 severity: SecuritySeverity::Warning,
552 component: "Environment Variables".to_string(),
553 description: format!("{} appears to be too short", var),
554 suggestion: format!(
555 "Ensure {} contains a sufficiently long, secure value",
556 var
557 ),
558 blocks_production: false,
559 });
560 }
561 }
562 }
563}
564
565#[derive(Debug, Clone)]
567pub struct SecurityAuditReport {
568 pub preset: SecurityPreset,
569 pub status: SecurityAuditStatus,
570 pub issues: Vec<SecurityIssue>,
571 pub critical_count: usize,
572 pub error_count: usize,
573 pub warning_count: usize,
574 pub recommendations: Vec<String>,
575}
576
577#[derive(Debug, Clone, PartialEq)]
579pub enum SecurityAuditStatus {
580 Passed,
582 Warning,
584 Failed,
586 Critical,
588}
589
590impl SecurityAuditReport {
591 pub fn print_report(&self) {
593 println!("đ Security Audit Report");
594 println!("========================");
595 println!("Preset: {:?}", self.preset);
596 println!("Status: {}", self.status_emoji());
597 println!();
598
599 if self.issues.is_empty() {
600 println!("â
No security issues found!");
601 } else {
602 println!("đ Issues Summary:");
603 println!(" Critical: {}", self.critical_count);
604 println!(" Errors: {}", self.error_count);
605 println!(" Warnings: {}", self.warning_count);
606 println!();
607
608 for issue in &self.issues {
609 println!(
610 "{} {}: {}",
611 issue.severity.emoji(),
612 issue.component,
613 issue.description
614 );
615 println!(" đĄ {}", issue.suggestion);
616 println!();
617 }
618 }
619
620 if !self.recommendations.is_empty() {
621 println!("đ Recommendations:");
622 for rec in &self.recommendations {
623 println!(" {}", rec);
624 }
625 println!();
626 }
627
628 println!("{}", self.get_summary_message());
629 }
630
631 fn status_emoji(&self) -> &str {
632 match self.status {
633 SecurityAuditStatus::Passed => "â
Passed",
634 SecurityAuditStatus::Warning => "â ī¸ Warning",
635 SecurityAuditStatus::Failed => "â Failed",
636 SecurityAuditStatus::Critical => "đ¨ Critical",
637 }
638 }
639
640 fn get_summary_message(&self) -> String {
641 match self.status {
642 SecurityAuditStatus::Passed => "đ Security audit passed! Your configuration meets security requirements.".to_string(),
643 SecurityAuditStatus::Warning => "â ī¸ Security audit completed with warnings. Consider addressing the issues above.".to_string(),
644 SecurityAuditStatus::Failed => "â Security audit failed. Please address the errors before deploying to production.".to_string(),
645 SecurityAuditStatus::Critical => "đ¨ Critical security issues found! Do not deploy to production until these are resolved.".to_string(),
646 }
647 }
648
649 pub fn is_production_ready(&self) -> bool {
651 matches!(
652 self.status,
653 SecurityAuditStatus::Passed | SecurityAuditStatus::Warning
654 )
655 }
656
657 pub fn get_blocking_issues(&self) -> Vec<&SecurityIssue> {
659 self.issues
660 .iter()
661 .filter(|issue| issue.blocks_production)
662 .collect()
663 }
664}
665
666impl SecuritySeverity {
667 fn emoji(&self) -> &str {
668 match self {
669 SecuritySeverity::Info => "âšī¸ ",
670 SecuritySeverity::Warning => "â ī¸ ",
671 SecuritySeverity::Error => "â",
672 SecuritySeverity::Critical => "đ¨",
673 }
674 }
675}
676
677impl std::fmt::Display for SecuritySeverity {
678 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
679 match self {
680 SecuritySeverity::Info => write!(f, "INFO"),
681 SecuritySeverity::Warning => write!(f, "WARNING"),
682 SecuritySeverity::Error => write!(f, "ERROR"),
683 SecuritySeverity::Critical => write!(f, "CRITICAL"),
684 }
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn test_development_preset_config() {
694 let config = SecurityPreset::Development.to_config();
695 assert_eq!(config.min_password_length, 6);
696 assert!(!config.require_password_complexity);
697 assert!(!config.secure_cookies);
698 assert!(!config.csrf_protection);
699 }
700
701 #[test]
702 fn test_balanced_preset_config() {
703 let config = SecurityPreset::Balanced.to_config();
704 assert_eq!(config.min_password_length, 8);
705 assert!(config.require_password_complexity);
706 assert!(config.secure_cookies);
707 assert!(config.csrf_protection);
708 }
709
710 #[test]
711 fn test_high_security_preset_config() {
712 let config = SecurityPreset::HighSecurity.to_config();
713 assert_eq!(config.min_password_length, 12);
714 assert!(config.secure_cookies);
715 assert!(config.csrf_protection);
716 assert!(matches!(config.cookie_same_site, CookieSameSite::Strict));
717 }
718
719 #[test]
720 fn test_paranoid_preset_config() {
721 let config = SecurityPreset::Paranoid.to_config();
722 assert_eq!(config.min_password_length, 16);
723 assert!(config.csrf_protection);
724 assert_eq!(config.session_timeout, minutes(30));
725 }
726
727 #[test]
728 fn test_presets_have_increasing_password_length() {
729 let dev = SecurityPreset::Development.to_config().min_password_length;
730 let bal = SecurityPreset::Balanced.to_config().min_password_length;
731 let high = SecurityPreset::HighSecurity.to_config().min_password_length;
732 let paranoid = SecurityPreset::Paranoid.to_config().min_password_length;
733 assert!(dev < bal);
734 assert!(bal < high);
735 assert!(high < paranoid);
736 }
737
738 #[test]
739 fn test_presets_have_decreasing_session_timeout() {
740 let dev = SecurityPreset::Development.to_config().session_timeout;
741 let bal = SecurityPreset::Balanced.to_config().session_timeout;
742 let high = SecurityPreset::HighSecurity.to_config().session_timeout;
743 let paranoid = SecurityPreset::Paranoid.to_config().session_timeout;
744 assert!(dev > bal);
745 assert!(bal > high);
746 assert!(high > paranoid);
747 }
748
749 #[test]
750 fn test_rate_limit_configs_are_valid() {
751 for preset in [
752 SecurityPreset::Development,
753 SecurityPreset::Balanced,
754 SecurityPreset::HighSecurity,
755 SecurityPreset::Paranoid,
756 ] {
757 let rl = preset.to_rate_limit_config();
758 assert!(rl.max_requests > 0);
759 assert!(!rl.window.is_zero());
760 }
761 }
762
763 #[test]
764 fn test_audit_configs_are_valid() {
765 for preset in [
766 SecurityPreset::Development,
767 SecurityPreset::Balanced,
768 SecurityPreset::HighSecurity,
769 SecurityPreset::Paranoid,
770 ] {
771 let ac = preset.to_audit_config();
772 if matches!(preset, SecurityPreset::HighSecurity | SecurityPreset::Paranoid) {
774 assert!(ac.enabled);
775 }
776 }
777 }
778
779 #[test]
780 fn test_security_recommendations_non_empty() {
781 for preset in [
782 SecurityPreset::Development,
783 SecurityPreset::Balanced,
784 SecurityPreset::HighSecurity,
785 SecurityPreset::Paranoid,
786 ] {
787 let recs = preset.get_security_recommendations();
788 assert!(!recs.is_empty());
789 }
790 }
791
792 #[test]
793 fn test_audit_report_production_ready() {
794 let report = SecurityAuditReport {
795 preset: SecurityPreset::Balanced,
796 status: SecurityAuditStatus::Passed,
797 issues: vec![],
798 critical_count: 0,
799 error_count: 0,
800 warning_count: 0,
801 recommendations: vec![],
802 };
803 assert!(report.is_production_ready());
804 }
805
806 #[test]
807 fn test_audit_report_not_production_ready_on_critical() {
808 let report = SecurityAuditReport {
809 preset: SecurityPreset::HighSecurity,
810 status: SecurityAuditStatus::Critical,
811 issues: vec![SecurityIssue {
812 severity: SecuritySeverity::Critical,
813 component: "test".into(),
814 description: "fail".into(),
815 suggestion: "fix".into(),
816 blocks_production: true,
817 }],
818 critical_count: 1,
819 error_count: 0,
820 warning_count: 0,
821 recommendations: vec![],
822 };
823 assert!(!report.is_production_ready());
824 assert_eq!(report.get_blocking_issues().len(), 1);
825 }
826
827 #[test]
828 fn test_severity_display() {
829 assert_eq!(format!("{}", SecuritySeverity::Critical), "CRITICAL");
830 assert_eq!(format!("{}", SecuritySeverity::Warning), "WARNING");
831 }
832}