use gossan_core::Target;
use reqwest::Client;
use secfinding::{Evidence, Finding, Severity};
pub async fn probe(client: &Client, target: &Target) -> anyhow::Result<Vec<Finding>> {
let Target::Web(asset) = target else {
return Ok(vec![]);
};
let base = asset.url.as_str();
let mut findings = Vec::new();
let resp = client.get(base).send().await?;
let headers = resp.headers().clone();
let cookies: Vec<String> = headers
.get_all("set-cookie")
.iter()
.filter_map(|v| v.to_str().ok().map(|s| s.to_string()))
.collect();
for cookie_str in &cookies {
let lower = cookie_str.to_lowercase();
let name = cookie_str
.split('=')
.next()
.unwrap_or("")
.trim()
.to_lowercase();
let is_session_cookie = name.contains("sess")
|| name.contains("auth")
|| name.contains("token")
|| name.contains("jwt")
|| name.contains("sid")
|| name.contains("user")
|| name.contains("login")
|| name.contains("uid")
|| name.contains("access")
|| name.contains("refresh")
|| name.contains("remember");
if !is_session_cookie {
continue;
}
if !lower.contains("; secure") && !lower.starts_with("secure") {
crate::try_push_finding(
crate::misconfig_finding(
target,
Severity::Medium,
format!(
"Cookie '{}' missing Secure flag",
cookie_str.split('=').next().unwrap_or("?")
),
format!(
"Session cookie is transmitted over HTTP as well as HTTPS. \
Network-layer attackers (MITM, coffee shop) can steal the session. \
Cookie: {}",
&cookie_str.chars().take(100).collect::<String>()
),
)
.evidence(Evidence::HttpResponse {
status: resp.status().as_u16(),
headers: vec![("set-cookie".into(), cookie_str.chars().take(120).collect::<String>().into())],
body_excerpt: None,
})
.tag("cookie")
.tag("session")
.tag("web"),
&mut findings,
);
}
if !lower.contains("httponly") {
crate::try_push_finding(
crate::misconfig_finding(
target,
Severity::Medium,
format!(
"Cookie '{}' missing HttpOnly flag",
cookie_str.split('=').next().unwrap_or("?")
),
format!(
"Session cookie is accessible via document.cookie — any XSS vulnerability \
can steal it. Add HttpOnly to prevent JS access. \
Cookie: {}",
&cookie_str.chars().take(100).collect::<String>()
),
)
.evidence(Evidence::HttpResponse {
status: resp.status().as_u16(),
headers: vec![("set-cookie".into(), cookie_str.chars().take(120).collect::<String>().into())],
body_excerpt: None,
})
.tag("cookie")
.tag("session")
.tag("web")
.tag("xss"),
&mut findings,
);
}
if !lower.contains("samesite") {
crate::try_push_finding(
crate::misconfig_finding(
target,
Severity::Low,
format!(
"Cookie '{}' missing SameSite attribute",
cookie_str.split('=').next().unwrap_or("?")
),
format!(
"No SameSite attribute — cookie is sent on cross-origin requests, \
enabling classic CSRF attacks on state-changing endpoints. \
Use SameSite=Strict or SameSite=Lax. \
Cookie: {}",
&cookie_str.chars().take(100).collect::<String>()
),
)
.evidence(Evidence::HttpResponse {
status: resp.status().as_u16(),
headers: vec![("set-cookie".into(), cookie_str.chars().take(120).collect::<String>().into())],
body_excerpt: None,
})
.tag("cookie")
.tag("csrf")
.tag("web"),
&mut findings,
);
} else if lower.contains("samesite=none") && !lower.contains("; secure") {
crate::try_push_finding(
crate::misconfig_finding(
target,
Severity::Low,
format!(
"Cookie '{}' SameSite=None without Secure",
cookie_str.split('=').next().unwrap_or("?")
),
"SameSite=None requires the Secure flag or browsers will reject the cookie. \
This is a misconfiguration that can cause auth failures.",
)
.tag("cookie")
.tag("web"),
&mut findings,
);
}
}
Ok(findings)
}