use crate::types::Cookie;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ConsentAnalysis {
pub banner_present: bool,
pub mechanism_type: ConsentMechanism,
pub opt_in_required: bool,
pub opt_out_available: bool,
pub granular_control: bool,
pub withdrawal_easy: bool,
pub consent_logged: bool,
pub cookie_categories: CookieCategories,
pub issues: Vec<String>,
pub recommendations: Vec<String>,
pub compliance_score: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ConsentMechanism {
None,
Implied,
OptOut,
OptIn,
GranularOptIn,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CookieCategories {
pub strictly_necessary: Vec<String>,
pub functional: Vec<String>,
pub performance: Vec<String>,
pub targeting: Vec<String>,
pub uncategorized: Vec<String>,
}
#[must_use]
pub fn analyze_consent_mechanism(cookies: &[Cookie]) -> ConsentAnalysis {
let mut analysis = ConsentAnalysis {
banner_present: false, mechanism_type: ConsentMechanism::None,
opt_in_required: false,
opt_out_available: false,
granular_control: false,
withdrawal_easy: false,
consent_logged: false,
cookie_categories: CookieCategories::default(),
issues: Vec::new(),
recommendations: Vec::new(),
compliance_score: 0.0,
};
categorize_cookies(cookies, &mut analysis.cookie_categories);
analysis.mechanism_type = detect_consent_mechanism(cookies);
let has_consent_cookie = cookies.iter().any(is_consent_cookie);
if has_consent_cookie {
analysis.consent_logged = true;
analysis.banner_present = true; }
match analysis.mechanism_type {
ConsentMechanism::None => {
analysis
.issues
.push("No consent mechanism detected for non-essential cookies".to_string());
analysis
.recommendations
.push("Implement cookie consent banner with opt-in mechanism".to_string());
}
ConsentMechanism::Implied => {
analysis.issues.push(
"Implied consent is not compliant with GDPR for non-essential cookies".to_string(),
);
analysis
.recommendations
.push("Switch to explicit opt-in consent mechanism".to_string());
}
ConsentMechanism::OptOut => {
analysis.opt_out_available = true;
analysis.issues.push(
"Opt-out consent may not be sufficient for GDPR (requires opt-in)".to_string(),
);
analysis
.recommendations
.push("Implement opt-in consent for GDPR compliance".to_string());
}
ConsentMechanism::OptIn => {
analysis.opt_in_required = true;
analysis
.recommendations
.push("Consider implementing granular consent for better user control".to_string());
}
ConsentMechanism::GranularOptIn => {
analysis.opt_in_required = true;
analysis.granular_control = true;
}
}
if (!analysis.cookie_categories.performance.is_empty()
|| !analysis.cookie_categories.targeting.is_empty())
&& !analysis.opt_in_required
{
analysis
.issues
.push("Non-essential cookies detected without opt-in consent requirement".to_string());
}
if analysis.consent_logged {
analysis.recommendations.push(
"Ensure users can easily withdraw consent (e.g., settings page, banner recall)"
.to_string(),
);
}
analysis.compliance_score = calculate_consent_compliance_score(&analysis);
analysis
}
fn categorize_cookies(cookies: &[Cookie], categories: &mut CookieCategories) {
for cookie in cookies {
let name_lower = cookie.name.to_lowercase();
if is_strictly_necessary(&name_lower) {
categories.strictly_necessary.push(cookie.name.clone());
} else if is_functional(&name_lower) {
categories.functional.push(cookie.name.clone());
} else if is_performance(&name_lower) {
categories.performance.push(cookie.name.clone());
} else if is_targeting(&name_lower) {
categories.targeting.push(cookie.name.clone());
} else {
categories.uncategorized.push(cookie.name.clone());
}
}
}
fn detect_consent_mechanism(cookies: &[Cookie]) -> ConsentMechanism {
let consent_patterns = [
"consent",
"cookie_consent",
"gdpr_consent",
"ccpa_consent",
"cookiecontrol",
"cookieconsent",
"optanon",
"onetrust",
"cmplz",
"complianz",
"cookie_notice",
];
let has_consent_cookie = cookies.iter().any(|c| {
let name_lower = c.name.to_lowercase();
consent_patterns.iter().any(|p| name_lower.contains(p))
});
if has_consent_cookie {
ConsentMechanism::OptIn
} else {
if cookies.iter().any(|c| is_targeting(&c.name.to_lowercase())) {
ConsentMechanism::Implied } else {
ConsentMechanism::None
}
}
}
fn is_consent_cookie(cookie: &Cookie) -> bool {
let consent_patterns = [
"consent",
"gdpr",
"ccpa",
"cookie_notice",
"cookie_consent",
"optanon",
"onetrust",
"cmplz",
"cookiecontrol",
];
let name_lower = cookie.name.to_lowercase();
consent_patterns.iter().any(|p| name_lower.contains(p))
}
fn is_strictly_necessary(name: &str) -> bool {
let patterns = [
"session",
"csrf",
"xsrf",
"auth",
"login",
"security",
"load_balancer",
"jsessionid",
"phpsessid",
"asp.net_sessionid",
];
patterns.iter().any(|p| name.contains(p))
}
fn is_functional(name: &str) -> bool {
let patterns = [
"lang",
"language",
"locale",
"timezone",
"currency",
"theme",
"preference",
"settings",
"region",
];
patterns.iter().any(|p| name.contains(p))
}
fn is_performance(name: &str) -> bool {
let patterns = [
"_ga",
"_gid",
"_gat",
"analytics",
"_hjid",
"_pk",
"matomo",
"piwik",
"clicky",
"statcounter",
];
patterns.iter().any(|p| name.contains(p))
}
fn is_targeting(name: &str) -> bool {
let patterns = [
"_fbp",
"_fbc",
"fbclid",
"doubleclick",
"adsense",
"adwords",
"ads",
"advertising",
"remarketing",
"conversion",
"campaign",
"criteo",
"outbrain",
"taboola",
"twitter",
"linkedin",
];
patterns.iter().any(|p| name.contains(p))
}
fn calculate_consent_compliance_score(analysis: &ConsentAnalysis) -> f32 {
let mut score = 0.0;
let mut total_points = 0.0;
total_points += 20.0;
if analysis.banner_present {
score += 20.0;
}
total_points += 30.0;
if analysis.opt_in_required {
score += 30.0;
}
total_points += 20.0;
if analysis.granular_control {
score += 20.0;
}
total_points += 15.0;
if analysis.consent_logged {
score += 15.0;
}
total_points += 15.0;
if analysis.withdrawal_easy {
score += 15.0;
}
#[allow(clippy::cast_precision_loss)]
let issue_penalty = (analysis.issues.len() as f32 * 10.0).min(30.0);
score = (score - issue_penalty).max(0.0);
(score / total_points * 100.0).min(100.0)
}
#[must_use]
pub fn generate_consent_recommendations(analysis: &ConsentAnalysis) -> Vec<String> {
let mut recommendations = Vec::new();
if !analysis.banner_present {
recommendations
.push("Implement a cookie consent banner visible on first visit".to_string());
}
if !analysis.opt_in_required {
recommendations.push(
"Require explicit opt-in consent for non-essential cookies (GDPR requirement)"
.to_string(),
);
}
if !analysis.granular_control {
recommendations.push(
"Provide granular consent options (Necessary, Functional, Analytics, Marketing)"
.to_string(),
);
}
if !analysis.consent_logged {
recommendations.push("Log consent decisions with timestamp for audit trail".to_string());
}
if !analysis.withdrawal_easy {
recommendations.push(
"Provide easy consent withdrawal mechanism (settings page or banner recall)"
.to_string(),
);
}
if !analysis.cookie_categories.targeting.is_empty() {
recommendations.push(
"Targeting/advertising cookies require explicit consent - ensure they're blocked until consent".to_string()
);
}
if !analysis.cookie_categories.uncategorized.is_empty() {
recommendations.push(format!(
"Categorize {} uncategorized cookies for proper consent management",
analysis.cookie_categories.uncategorized.len()
));
}
recommendations
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_strictly_necessary() {
assert!(is_strictly_necessary("sessionid"));
assert!(is_strictly_necessary("csrf_token"));
assert!(!is_strictly_necessary("_ga"));
}
#[test]
fn test_is_functional() {
assert!(is_functional("language"));
assert!(is_functional("theme_preference"));
assert!(!is_functional("_ga"));
}
#[test]
fn test_is_performance() {
assert!(is_performance("_ga"));
assert!(is_performance("_gid"));
assert!(!is_performance("sessionid"));
}
#[test]
fn test_is_targeting() {
assert!(is_targeting("_fbp"));
assert!(is_targeting("doubleclick_id"));
assert!(!is_targeting("sessionid"));
}
#[test]
fn test_consent_cookie_detection() {
let cookie = Cookie::new("cookie_consent".to_string(), "granted".to_string());
assert!(is_consent_cookie(&cookie));
let cookie = Cookie::new("sessionid".to_string(), "abc123".to_string());
assert!(!is_consent_cookie(&cookie));
}
#[test]
fn test_categorize_cookies() {
let cookies = vec![
Cookie::new("sessionid".to_string(), "abc".to_string()),
Cookie::new("language".to_string(), "en".to_string()),
Cookie::new("_ga".to_string(), "GA1.2.123".to_string()),
Cookie::new("_fbp".to_string(), "fb.1.123".to_string()),
];
let mut categories = CookieCategories::default();
categorize_cookies(&cookies, &mut categories);
assert_eq!(categories.strictly_necessary.len(), 1);
assert_eq!(categories.functional.len(), 1);
assert_eq!(categories.performance.len(), 1);
assert_eq!(categories.targeting.len(), 1);
}
#[test]
fn test_analyze_consent_mechanism() {
let cookies = vec![
Cookie::new("cookie_consent".to_string(), "granted".to_string()),
Cookie::new("_ga".to_string(), "GA1.2.123".to_string()),
];
let analysis = analyze_consent_mechanism(&cookies);
assert!(analysis.consent_logged);
assert!(analysis.compliance_score > 0.0);
}
#[test]
fn test_calculate_compliance_score() {
let mut analysis = ConsentAnalysis {
banner_present: true,
mechanism_type: ConsentMechanism::GranularOptIn,
opt_in_required: true,
opt_out_available: true,
granular_control: true,
withdrawal_easy: true,
consent_logged: true,
cookie_categories: CookieCategories::default(),
issues: Vec::new(),
recommendations: Vec::new(),
compliance_score: 0.0,
};
let score = calculate_consent_compliance_score(&analysis);
assert!(score > 90.0);
analysis.opt_in_required = false;
analysis.granular_control = false;
let low_score = calculate_consent_compliance_score(&analysis);
assert!(low_score < score);
}
}