use super::headers::{HeaderIssue, SecurityHeaderChecker};
use crate::Result;
use crate::utils::network::Target;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderAnalysisResult {
pub headers: HashMap<String, String>,
pub issues: Vec<HeaderIssue>,
pub score: u8,
pub grade: SecurityGrade,
#[serde(skip_serializing_if = "Option::is_none")]
pub hsts_analysis: Option<super::headers_advanced::HstsAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hpkp_analysis: Option<super::headers_advanced::HpkpAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cookie_analysis: Option<super::headers_advanced::CookieAnalysis>,
#[serde(skip_serializing_if = "Option::is_none")]
pub datetime_check: Option<super::headers_advanced::DateTimeCheck>,
#[serde(skip_serializing_if = "Option::is_none")]
pub banner_detection: Option<super::headers_advanced::BannerDetection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reverse_proxy_detection: Option<super::headers_advanced::ReverseProxyDetection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub http_status_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_location: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub redirect_chain: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub server_hostname: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SecurityGrade {
A,
B,
C,
D,
F,
}
#[derive(Debug, Clone)]
struct ResponseMetadata {
headers: HashMap<String, String>,
status_code: u16,
redirect_location: Option<String>,
server_hostname: Option<String>,
}
pub struct HeaderAnalyzer {
target: Target,
timeout: Duration,
custom_headers: Vec<(String, String)>,
user_agent: String,
}
impl HeaderAnalyzer {
pub fn new(target: Target) -> Self {
Self {
target,
timeout: Duration::from_secs(10),
custom_headers: Vec::new(),
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36".to_string(),
}
}
pub fn with_custom_headers(target: Target, custom_headers: Vec<(String, String)>) -> Self {
Self {
target,
timeout: Duration::from_secs(10),
custom_headers,
user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36".to_string(),
}
}
pub fn with_user_agent(mut self, user_agent: String) -> Self {
self.user_agent = user_agent;
self
}
pub async fn analyze(&self) -> Result<HeaderAnalysisResult> {
let metadata = self.fetch_headers().await?;
let issues = SecurityHeaderChecker::check_all_headers(&metadata.headers);
let score = Self::calculate_score(&issues);
let grade = Self::calculate_grade(score);
use super::headers_advanced;
let hsts_analysis = Some(headers_advanced::parse_hsts(&metadata.headers));
let hpkp_analysis = Some(headers_advanced::parse_hpkp(&metadata.headers));
let set_cookie_headers: Vec<String> = metadata
.headers
.iter()
.filter(|(k, _)| k.eq_ignore_ascii_case("set-cookie"))
.map(|(_, v)| v.clone())
.collect();
let cookie_analysis = if !set_cookie_headers.is_empty() {
Some(headers_advanced::parse_cookies(&set_cookie_headers))
} else {
None
};
let datetime_check = Some(headers_advanced::check_datetime(&metadata.headers));
let banner_detection = Some(headers_advanced::detect_banners(&metadata.headers));
let reverse_proxy_detection =
Some(headers_advanced::detect_reverse_proxy(&metadata.headers));
Ok(HeaderAnalysisResult {
headers: metadata.headers,
issues,
score,
grade,
hsts_analysis,
hpkp_analysis,
cookie_analysis,
datetime_check,
banner_detection,
reverse_proxy_detection,
http_status_code: Some(metadata.status_code),
redirect_location: metadata.redirect_location,
redirect_chain: Vec::new(), server_hostname: metadata.server_hostname,
})
}
async fn fetch_headers(&self) -> Result<ResponseMetadata> {
let url = format!("https://{}:{}/", self.target.hostname, self.target.port);
let client = reqwest::Client::builder()
.timeout(self.timeout)
.danger_accept_invalid_certs(true) .user_agent(&self.user_agent)
.redirect(reqwest::redirect::Policy::none()) .build()?;
let mut request = client.get(&url);
for (name, value) in &self.custom_headers {
request = request.header(name, value);
}
let response = request
.send()
.await
.map_err(|e| anyhow::anyhow!("Failed to fetch headers: {}", e))?;
let status_code = response.status().as_u16();
let redirect_location = if response.status().is_redirection() {
tracing::info!(
"HTTP redirect detected: {} {}",
status_code,
response.status().canonical_reason().unwrap_or("Unknown")
);
let location = response
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
if let Some(ref location_str) = location {
tracing::info!("Redirect location: {}", location_str);
}
location
} else {
None
};
let server_hostname = response
.headers()
.get("server")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let mut headers = HashMap::new();
for (name, value) in response.headers().iter() {
if let Ok(value_str) = value.to_str() {
headers.insert(name.to_string(), value_str.to_string());
}
}
Ok(ResponseMetadata {
headers,
status_code,
redirect_location,
server_hostname,
})
}
fn calculate_score(issues: &[HeaderIssue]) -> u8 {
use super::headers::IssueSeverity;
let mut score: u8 = 100;
for issue in issues {
let deduction = match issue.severity {
IssueSeverity::Critical => 20,
IssueSeverity::High => 15,
IssueSeverity::Medium => 10,
IssueSeverity::Low => 5,
IssueSeverity::Info => 0,
};
score = score.saturating_sub(deduction);
}
score
}
fn calculate_grade(score: u8) -> SecurityGrade {
match score {
90..=100 => SecurityGrade::A,
75..=89 => SecurityGrade::B,
60..=74 => SecurityGrade::C,
40..=59 => SecurityGrade::D,
_ => SecurityGrade::F,
}
}
}
impl HeaderAnalysisResult {
pub fn summary(&self) -> String {
format!(
"Security Headers Grade: {:?} (Score: {}/100, {} issues)",
self.grade,
self.score,
self.issues.len()
)
}
pub fn critical_issues(&self) -> Vec<&HeaderIssue> {
use super::headers::IssueSeverity;
self.issues
.iter()
.filter(|i| matches!(i.severity, IssueSeverity::Critical | IssueSeverity::High))
.collect()
}
pub fn has_serious_issues(&self) -> bool {
!self.critical_issues().is_empty()
}
}
impl SecurityGrade {
pub fn color(&self) -> &'static str {
match self {
SecurityGrade::A => "green",
SecurityGrade::B => "blue",
SecurityGrade::C => "yellow",
SecurityGrade::D => "orange",
SecurityGrade::F => "red",
}
}
pub fn description(&self) -> &'static str {
match self {
SecurityGrade::A => "Excellent security headers",
SecurityGrade::B => "Good security headers",
SecurityGrade::C => "Fair security headers",
SecurityGrade::D => "Poor security headers",
SecurityGrade::F => "Failing security headers",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::headers::IssueType;
#[test]
fn test_score_calculation() {
let issues = vec![HeaderIssue {
header_name: "HSTS".to_string(),
severity: crate::http::headers::IssueSeverity::High,
issue_type: IssueType::Missing,
description: "Missing HSTS".to_string(),
recommendation: "Add HSTS".to_string(),
preload_status: None,
}];
let score = HeaderAnalyzer::calculate_score(&issues);
assert_eq!(score, 85); }
#[test]
fn test_grade_calculation() {
assert_eq!(HeaderAnalyzer::calculate_grade(95), SecurityGrade::A);
assert_eq!(HeaderAnalyzer::calculate_grade(80), SecurityGrade::B);
assert_eq!(HeaderAnalyzer::calculate_grade(65), SecurityGrade::C);
assert_eq!(HeaderAnalyzer::calculate_grade(50), SecurityGrade::D);
assert_eq!(HeaderAnalyzer::calculate_grade(30), SecurityGrade::F);
}
#[test]
fn test_security_grade_color() {
assert_eq!(SecurityGrade::A.color(), "green");
assert_eq!(SecurityGrade::F.color(), "red");
}
}