1use regex::Regex;
2use reqwest::{Client, Method};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::Duration;
6
7struct WafSignature {
10 name: &'static str,
11 headers: &'static [&'static str],
12 server: &'static [&'static str],
13}
14
15const WAF_SIGNATURES: &[WafSignature] = &[
16 WafSignature {
17 name: "Cloudflare",
18 headers: &["cf-ray", "cf-cache-status", "__cfduid"],
19 server: &["cloudflare"],
20 },
21 WafSignature {
22 name: "Akamai",
23 headers: &["akamai-transformed", "akamai-cache-status"],
24 server: &["akamaighost"],
25 },
26 WafSignature {
27 name: "Imperva Incapsula",
28 headers: &["x-iinfo", "incap_ses"],
29 server: &["imperva"],
30 },
31 WafSignature {
32 name: "Sucuri",
33 headers: &["x-sucuri-id", "x-sucuri-cache"],
34 server: &["sucuri"],
35 },
36 WafSignature {
37 name: "Barracuda",
38 headers: &["barra"],
39 server: &["barracuda"],
40 },
41 WafSignature {
42 name: "F5 BIG-IP",
43 headers: &["f5-http-lb", "bigip"],
44 server: &["bigip", "f5"],
45 },
46 WafSignature {
47 name: "AWS WAF",
48 headers: &["x-amz-cf-id", "x-amzn-requestid"],
49 server: &["awselb"],
50 },
51];
52
53const SECURITY_HEADERS: &[(&str, &str)] = &[
55 ("strict-transport-security", "Critical"),
56 ("content-security-policy", "Critical"),
57 ("x-frame-options", "High"),
58 ("x-content-type-options", "Medium"),
59 ("x-xss-protection", "Medium"),
60 ("referrer-policy", "Medium"),
61 ("permissions-policy", "Medium"),
62];
63
64const ERROR_PATTERNS: &[(&str, &str)] = &[
66 ("fatal error", "PHP Fatal Error"),
67 ("warning.*mysql", "MySQL Warning"),
68 ("error.*sql", "SQL Error"),
69];
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct SecurityAnalysisResult {
75 pub domain: String,
76 pub https_available: bool,
77 pub https_redirect: bool,
78 pub waf_detection: WafDetectionResult,
79 pub security_headers: SecurityHeadersResult,
80 pub ssl_analysis: SslAnalysisResult,
81 pub cors_policy: CorsPolicyResult,
82 pub cookie_security: CookieSecurityResult,
83 pub http_methods: HttpMethodsResult,
84 pub server_information: ServerInfoResult,
85 pub vulnerability_scan: VulnScanResult,
86 pub security_score: SecurityScoreResult,
87 pub recommendations: Vec<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct WafMatch {
92 pub provider: String,
93 pub confidence: String,
94 pub detection_methods: Vec<String>,
95 pub score: u32,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct WafDetectionResult {
100 pub detected: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub primary_waf: Option<WafMatch>,
103 pub all_detected: Vec<WafMatch>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct HeaderAnalysis {
108 pub present: bool,
109 pub value: String,
110 pub importance: String,
111 pub security_level: String,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SecurityHeadersResult {
116 pub headers: HashMap<String, HeaderAnalysis>,
117 pub score: u32,
118 pub missing_critical: Vec<String>,
119 pub missing_high: Vec<String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SslAnalysisResult {
124 pub ssl_available: bool,
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub protocol_version: Option<String>,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub cipher_suite: Option<String>,
129 pub cipher_strength: String,
130 pub overall_grade: String,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub subject: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub issuer: Option<String>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CorsPolicyResult {
139 pub configured: bool,
140 pub headers: HashMap<String, String>,
141 pub issues: Vec<String>,
142 pub security_level: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct CookieSecurityResult {
147 pub cookies_present: bool,
148 pub security_issues: Vec<String>,
149 pub security_score: u32,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct HttpMethodsResult {
154 pub methods_detected: bool,
155 pub allowed_methods: Vec<String>,
156 pub dangerous_methods: Vec<String>,
157 pub security_risk: String,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ServerInfoResult {
162 pub server_headers: HashMap<String, String>,
163 pub information_disclosure: Vec<String>,
164 pub disclosure_count: usize,
165 pub security_level: String,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct VulnerabilityFound {
170 pub vuln_type: String,
171 pub severity: String,
172 pub description: String,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct VulnScanResult {
177 pub vulnerabilities_found: usize,
178 pub vulnerabilities: Vec<VulnerabilityFound>,
179 pub risk_level: String,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct SecurityScoreResult {
184 pub overall_score: u32,
185 pub grade: String,
186 pub risk_level: String,
187 pub score_breakdown: HashMap<String, u32>,
188}
189
190pub async fn analyze_security(
193 domain: &str,
194) -> Result<SecurityAnalysisResult, Box<dyn std::error::Error + Send + Sync>> {
195 let clean = if domain.starts_with("http://") || domain.starts_with("https://") {
196 domain
197 .split("//")
198 .nth(1)
199 .unwrap_or(domain)
200 .split('/')
201 .next()
202 .unwrap_or(domain)
203 .to_string()
204 } else {
205 domain.to_string()
206 };
207
208 let client = Client::builder()
209 .timeout(Duration::from_secs(30))
210 .danger_accept_invalid_certs(true)
211 .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
212 .build()?;
213
214 let http_url = format!("http://{}", clean);
216 let https_url = format!("https://{}", clean);
217
218 let redir_client = Client::builder()
220 .timeout(Duration::from_secs(15))
221 .danger_accept_invalid_certs(true)
222 .redirect(reqwest::redirect::Policy::none())
223 .user_agent("Mozilla/5.0")
224 .build()?;
225
226 let mut https_redirect = false;
227 if let Ok(resp) = redir_client.get(&http_url).send().await {
228 let status = resp.status().as_u16();
229 if [301, 302, 307, 308].contains(&status) {
230 if let Some(loc) = resp.headers().get("location") {
231 if let Ok(l) = loc.to_str() {
232 if l.starts_with("https://") {
233 https_redirect = true;
234 }
235 }
236 }
237 }
238 }
239
240 let https_resp = client.get(&https_url).send().await;
242 let https_available = https_resp.is_ok();
243
244 let primary = if let Ok(r) = https_resp {
245 r
246 } else {
247 client.get(&http_url).send().await?
248 };
249
250 let resp_url = primary.url().to_string();
251 let headers = primary.headers().clone();
252 let body_text = primary.text().await.unwrap_or_default();
253
254 let waf_detection = detect_waf(&headers);
256
257 let security_headers = analyze_security_headers(&headers);
259
260 let ssl_analysis = analyze_ssl(&clean).await;
262
263 let cors_policy = analyze_cors(&headers);
265
266 let cookie_security = analyze_cookies(&headers);
268
269 let http_methods = detect_methods(&client, &https_url).await;
271
272 let server_information = analyze_server_info(&headers);
274
275 let vulnerability_scan = perform_vuln_scan(&resp_url, &body_text);
277
278 let security_score = calculate_score(
280 &security_headers,
281 &ssl_analysis,
282 &waf_detection,
283 &vulnerability_scan,
284 );
285 let recommendations = generate_recommendations(
286 &security_headers,
287 &ssl_analysis,
288 &waf_detection,
289 https_available,
290 https_redirect,
291 );
292
293 Ok(SecurityAnalysisResult {
294 domain: clean,
295 https_available,
296 https_redirect,
297 waf_detection,
298 security_headers,
299 ssl_analysis,
300 cors_policy,
301 cookie_security,
302 http_methods,
303 server_information,
304 vulnerability_scan,
305 security_score,
306 recommendations,
307 })
308}
309
310fn detect_waf(headers: &reqwest::header::HeaderMap) -> WafDetectionResult {
313 let headers_str = format!("{:?}", headers).to_lowercase();
314 let server_header = headers
315 .get("server")
316 .and_then(|v| v.to_str().ok())
317 .unwrap_or("")
318 .to_lowercase();
319
320 let mut detected = Vec::new();
321
322 for sig in WAF_SIGNATURES {
323 let mut confidence: u32 = 0;
324 let mut methods = Vec::new();
325
326 for h in sig.headers {
327 if headers_str.contains(h) {
328 confidence += 40;
329 methods.push(format!("Header: {}", h));
330 }
331 }
332 for s in sig.server {
333 if server_header.contains(s) {
334 confidence += 30;
335 methods.push(format!("Server: {}", s));
336 }
337 }
338
339 if confidence > 0 {
340 let conf_str = if confidence >= 50 {
341 "High"
342 } else if confidence >= 30 {
343 "Medium"
344 } else {
345 "Low"
346 };
347 detected.push(WafMatch {
348 provider: sig.name.to_string(),
349 confidence: conf_str.into(),
350 detection_methods: methods,
351 score: confidence,
352 });
353 }
354 }
355
356 detected.sort_by(|a, b| b.score.cmp(&a.score));
357
358 WafDetectionResult {
359 detected: !detected.is_empty(),
360 primary_waf: detected.first().cloned(),
361 all_detected: detected,
362 }
363}
364
365fn analyze_security_headers(headers: &reqwest::header::HeaderMap) -> SecurityHeadersResult {
368 let mut analysis = HashMap::new();
369 let mut total_score: u32 = 0;
370 let mut max_score: u32 = 0;
371 let mut missing_critical = Vec::new();
372 let mut missing_high = Vec::new();
373
374 for &(name, importance) in SECURITY_HEADERS {
375 let present = headers.get(name).is_some();
376 let value = headers
377 .get(name)
378 .and_then(|v| v.to_str().ok())
379 .unwrap_or("Not Set")
380 .to_string();
381
382 let security_level = if present {
383 "Good".into()
384 } else if importance == "Critical" {
385 "Critical".into()
386 } else {
387 "Medium".into()
388 };
389
390 let weight = match importance {
391 "Critical" => 30,
392 "High" => 20,
393 _ => 10,
394 };
395 max_score += weight;
396 if present {
397 total_score += weight;
398 } else if importance == "Critical" {
399 missing_critical.push(name.to_string());
400 } else if importance == "High" {
401 missing_high.push(name.to_string());
402 }
403
404 analysis.insert(
405 name.to_string(),
406 HeaderAnalysis {
407 present,
408 value,
409 importance: importance.into(),
410 security_level,
411 },
412 );
413 }
414
415 let score = if max_score > 0 {
416 total_score * 100 / max_score
417 } else {
418 0
419 };
420
421 SecurityHeadersResult {
422 headers: analysis,
423 score,
424 missing_critical,
425 missing_high,
426 }
427}
428
429async fn analyze_ssl(domain: &str) -> SslAnalysisResult {
432 let output = match tokio::process::Command::new("openssl")
433 .args([
434 "s_client",
435 "-connect",
436 &format!("{}:443", domain),
437 "-servername",
438 domain,
439 ])
440 .stdin(std::process::Stdio::null())
441 .stdout(std::process::Stdio::piped())
442 .stderr(std::process::Stdio::piped())
443 .output()
444 .await
445 {
446 Ok(o) => String::from_utf8_lossy(&o.stdout).to_string(),
447 Err(_) => {
448 return SslAnalysisResult {
449 ssl_available: false,
450 protocol_version: None,
451 cipher_suite: None,
452 cipher_strength: "Unknown".into(),
453 overall_grade: "F".into(),
454 subject: None,
455 issuer: None,
456 }
457 }
458 };
459
460 if !output.contains("CONNECTED") {
461 return SslAnalysisResult {
462 ssl_available: false,
463 protocol_version: None,
464 cipher_suite: None,
465 cipher_strength: "Unknown".into(),
466 overall_grade: "F".into(),
467 subject: None,
468 issuer: None,
469 };
470 }
471
472 let protocol = Regex::new(r"Protocol\s*:\s*(.+)")
473 .ok()
474 .and_then(|r| r.captures(&output))
475 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
476
477 let cipher_suite = Regex::new(r"Cipher\s*:\s*(.+)")
478 .ok()
479 .and_then(|r| r.captures(&output))
480 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
481
482 let subject = Regex::new(r"subject=.*?CN\s*=\s*([^\n/,]+)")
483 .ok()
484 .and_then(|r| r.captures(&output))
485 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
486
487 let issuer = Regex::new(r"issuer=.*?CN\s*=\s*([^\n/,]+)")
488 .ok()
489 .and_then(|r| r.captures(&output))
490 .and_then(|c| c.get(1).map(|m| m.as_str().trim().to_string()));
491
492 let cipher_strength = match &cipher_suite {
494 Some(c) if c.contains("AES256") || c.contains("CHACHA20") || c.contains("TLS_AES_256") => {
495 "Strong"
496 }
497 Some(c) if c.contains("AES128") => "Medium",
498 Some(c) if c.contains("DES") || c.contains("RC4") || c.contains("NULL") => "Weak",
499 _ => "Unknown",
500 };
501
502 let proto_str = protocol.as_deref().unwrap_or("");
504 let grade = if proto_str.contains("TLSv1.3") {
505 "A+"
506 } else if proto_str.contains("TLSv1.2") && cipher_strength == "Strong" {
507 "A"
508 } else if proto_str.contains("TLSv1.2") {
509 "B"
510 } else if proto_str.contains("TLSv1.1") || proto_str.contains("TLSv1") {
511 "C"
512 } else {
513 "F"
514 };
515
516 SslAnalysisResult {
517 ssl_available: true,
518 protocol_version: protocol,
519 cipher_suite,
520 cipher_strength: cipher_strength.into(),
521 overall_grade: grade.into(),
522 subject,
523 issuer,
524 }
525}
526
527fn analyze_cors(headers: &reqwest::header::HeaderMap) -> CorsPolicyResult {
530 let cors_keys = [
531 "access-control-allow-origin",
532 "access-control-allow-methods",
533 "access-control-allow-headers",
534 "access-control-allow-credentials",
535 ];
536
537 let mut cors_headers = HashMap::new();
538 let mut configured = false;
539 let mut issues = Vec::new();
540
541 for &key in &cors_keys {
542 let val = headers
543 .get(key)
544 .and_then(|v| v.to_str().ok())
545 .unwrap_or("Not Set")
546 .to_string();
547 if val != "Not Set" {
548 configured = true;
549 }
550 cors_headers.insert(key.to_string(), val);
551 }
552
553 let origin = cors_headers
554 .get("access-control-allow-origin")
555 .map(|s| s.as_str())
556 .unwrap_or("Not Set");
557 let creds = cors_headers
558 .get("access-control-allow-credentials")
559 .map(|s| s.as_str())
560 .unwrap_or("Not Set");
561
562 if origin == "*" && creds == "true" {
563 issues.push("Critical: Wildcard origin with credentials allowed".into());
564 } else if origin == "*" {
565 issues.push("Warning: Wildcard origin allows all domains".into());
566 }
567
568 let security_level = if issues.is_empty() {
569 "High"
570 } else if issues.len() <= 1 {
571 "Medium"
572 } else {
573 "Low"
574 };
575
576 CorsPolicyResult {
577 configured,
578 headers: cors_headers,
579 issues,
580 security_level: security_level.into(),
581 }
582}
583
584fn analyze_cookies(headers: &reqwest::header::HeaderMap) -> CookieSecurityResult {
587 let cookie_val = match headers.get("set-cookie").and_then(|v| v.to_str().ok()) {
588 Some(c) => c.to_string(),
589 None => {
590 return CookieSecurityResult {
591 cookies_present: false,
592 security_issues: vec![],
593 security_score: 100,
594 }
595 }
596 };
597
598 let mut issues = Vec::new();
599 if !cookie_val.contains("Secure") {
600 issues.push("Missing Secure flag".into());
601 }
602 if !cookie_val.contains("HttpOnly") {
603 issues.push("Missing HttpOnly flag".into());
604 }
605 if !cookie_val.contains("SameSite") {
606 issues.push("Missing SameSite attribute".into());
607 }
608
609 let score = 100u32.saturating_sub(issues.len() as u32 * 25);
610
611 CookieSecurityResult {
612 cookies_present: true,
613 security_issues: issues,
614 security_score: score,
615 }
616}
617
618async fn detect_methods(client: &Client, url: &str) -> HttpMethodsResult {
621 let dangerous = ["DELETE", "PUT", "PATCH", "TRACE", "CONNECT"];
622
623 match client.request(Method::OPTIONS, url).send().await {
624 Ok(resp) => {
625 let allow = resp
626 .headers()
627 .get("allow")
628 .and_then(|v| v.to_str().ok())
629 .unwrap_or("");
630 let methods: Vec<String> = allow
631 .split(',')
632 .map(|m| m.trim().to_string())
633 .filter(|m| !m.is_empty())
634 .collect();
635
636 let found_dangerous: Vec<String> = methods
637 .iter()
638 .filter(|m| dangerous.contains(&m.to_uppercase().as_str()))
639 .cloned()
640 .collect();
641
642 let risk = if !found_dangerous.is_empty() {
643 "High"
644 } else {
645 "Low"
646 };
647
648 HttpMethodsResult {
649 methods_detected: true,
650 allowed_methods: methods,
651 dangerous_methods: found_dangerous,
652 security_risk: risk.into(),
653 }
654 }
655 Err(_) => HttpMethodsResult {
656 methods_detected: false,
657 allowed_methods: vec![],
658 dangerous_methods: vec![],
659 security_risk: "Unknown".into(),
660 },
661 }
662}
663
664fn analyze_server_info(headers: &reqwest::header::HeaderMap) -> ServerInfoResult {
667 let disclosure_headers = [
668 ("server", "Web server version disclosed"),
669 ("x-powered-by", "Technology stack disclosed"),
670 ];
671
672 let mut server_headers = HashMap::new();
673 let mut issues = Vec::new();
674
675 for &(header, issue) in &disclosure_headers {
676 if let Some(val) = headers.get(header).and_then(|v| v.to_str().ok()) {
677 server_headers.insert(header.to_string(), val.to_string());
678 issues.push(issue.to_string());
679 }
680 }
681
682 let count = issues.len();
683 let level = if count > 2 {
684 "High"
685 } else if count > 0 {
686 "Medium"
687 } else {
688 "Good"
689 };
690
691 ServerInfoResult {
692 server_headers,
693 information_disclosure: issues,
694 disclosure_count: count,
695 security_level: level.into(),
696 }
697}
698
699fn perform_vuln_scan(resp_url: &str, body: &str) -> VulnScanResult {
702 let mut vulns = Vec::new();
703
704 if !resp_url.starts_with("https://") {
706 vulns.push(VulnerabilityFound {
707 vuln_type: "Insecure Transport".into(),
708 severity: "High".into(),
709 description: "Site not enforcing HTTPS".into(),
710 });
711 }
712
713 for &(pattern, desc) in ERROR_PATTERNS {
715 if let Ok(rx) = Regex::new(&format!("(?i){}", pattern)) {
716 if rx.is_match(body) {
717 vulns.push(VulnerabilityFound {
718 vuln_type: "Information Disclosure".into(),
719 severity: "Low".into(),
720 description: format!("{} detected in response", desc),
721 });
722 }
723 }
724 }
725
726 let risk = calculate_risk_level(&vulns);
727
728 VulnScanResult {
729 vulnerabilities_found: vulns.len(),
730 vulnerabilities: vulns,
731 risk_level: risk,
732 }
733}
734
735fn calculate_risk_level(vulns: &[VulnerabilityFound]) -> String {
736 if vulns.is_empty() {
737 return "Low".into();
738 }
739 let total: u32 = vulns
740 .iter()
741 .map(|v| match v.severity.as_str() {
742 "High" => 3,
743 "Medium" => 2,
744 _ => 1,
745 })
746 .sum();
747
748 if total >= 6 {
749 "Critical".into()
750 } else if total >= 4 {
751 "High".into()
752 } else if total >= 2 {
753 "Medium".into()
754 } else {
755 "Low".into()
756 }
757}
758
759fn calculate_score(
762 headers: &SecurityHeadersResult,
763 ssl: &SslAnalysisResult,
764 waf: &WafDetectionResult,
765 vulns: &VulnScanResult,
766) -> SecurityScoreResult {
767 let mut breakdown = HashMap::new();
768 let mut total: f64 = 100.0;
769
770 let h_score = headers.score;
772 breakdown.insert("security_headers".into(), h_score);
773 total -= (100.0 - h_score as f64) * 0.4;
774
775 let ssl_score: u32 = match ssl.overall_grade.as_str() {
777 "A+" => 100,
778 "A" => 90,
779 "B" => 75,
780 "C" => 60,
781 "D" => 40,
782 _ => 0,
783 };
784 breakdown.insert("ssl_tls".into(), ssl_score);
785 total -= (100.0 - ssl_score as f64) * 0.3;
786
787 let waf_score: u32 = if waf.detected { 100 } else { 60 };
789 breakdown.insert("waf_protection".into(), waf_score);
790 total -= (100.0 - waf_score as f64) * 0.15;
791
792 let vuln_score = 100u32.saturating_sub(vulns.vulnerabilities_found as u32 * 20);
794 breakdown.insert("vulnerabilities".into(), vuln_score);
795 total -= (100.0 - vuln_score as f64) * 0.15;
796
797 let final_score = total.clamp(0.0, 100.0) as u32;
798
799 let grade = if final_score >= 95 {
800 "A+"
801 } else if final_score >= 90 {
802 "A"
803 } else if final_score >= 80 {
804 "B"
805 } else if final_score >= 70 {
806 "C"
807 } else if final_score >= 60 {
808 "D"
809 } else {
810 "F"
811 };
812
813 let risk = if final_score >= 85 {
814 "Low Risk"
815 } else if final_score >= 70 {
816 "Medium Risk"
817 } else if final_score >= 50 {
818 "High Risk"
819 } else {
820 "Critical Risk"
821 };
822
823 SecurityScoreResult {
824 overall_score: final_score,
825 grade: grade.into(),
826 risk_level: risk.into(),
827 score_breakdown: breakdown,
828 }
829}
830
831fn generate_recommendations(
834 headers: &SecurityHeadersResult,
835 ssl: &SslAnalysisResult,
836 waf: &WafDetectionResult,
837 https_available: bool,
838 https_redirect: bool,
839) -> Vec<String> {
840 let mut recs = Vec::new();
841
842 if !headers.missing_critical.is_empty() {
843 recs.push(format!(
844 "CRITICAL: Implement missing security headers: {}",
845 headers.missing_critical.join(", ")
846 ));
847 }
848 if !headers.missing_high.is_empty() {
849 recs.push(format!(
850 "HIGH: Add security headers: {}",
851 headers.missing_high.join(", ")
852 ));
853 }
854
855 match ssl.overall_grade.as_str() {
856 "D" | "F" => recs.push("CRITICAL: Upgrade SSL/TLS configuration".into()),
857 "C" => recs.push("MEDIUM: Consider improving SSL/TLS configuration".into()),
858 _ => {}
859 }
860
861 if !waf.detected {
862 recs.push("MEDIUM: Consider implementing a Web Application Firewall (WAF)".into());
863 }
864
865 if !https_available {
866 recs.push("CRITICAL: Enable HTTPS for secure communication".into());
867 } else if !https_redirect {
868 recs.push("MEDIUM: Implement automatic HTTP to HTTPS redirect".into());
869 }
870
871 recs.truncate(10);
872 recs
873}