use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Config {
pub scan: ScanConfig,
pub analysis: AnalysisConfig,
pub report: ReportConfig,
pub storage: StorageConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ScanConfig {
pub max_pages: usize,
pub max_depth: usize,
pub timeout: Duration,
pub user_agent: String,
pub follow_redirects: bool,
pub max_redirects: usize,
pub verify_ssl: bool,
pub proxy: Option<String>,
pub headers: Vec<(String, String)>,
pub analyze_js_cookies: bool,
pub execute_javascript: bool,
pub include_domains: Vec<String>,
pub exclude_domains: Vec<String>,
pub rate_limit: Option<u32>,
pub concurrency: usize,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
max_pages: 100,
max_depth: 3,
timeout: Duration::from_secs(30),
user_agent: "ICOokForms/1.0".to_string(),
follow_redirects: true,
max_redirects: 5,
verify_ssl: true,
proxy: None,
headers: Vec::new(),
analyze_js_cookies: true,
execute_javascript: false,
include_domains: Vec::new(),
exclude_domains: Vec::new(),
rate_limit: None,
concurrency: 4,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct AnalysisConfig {
pub security_enabled: bool,
pub compliance_enabled: bool,
pub tracking_enabled: bool,
pub fingerprinting_enabled: bool,
pub supply_chain_enabled: bool,
pub min_severity: crate::types::Severity,
pub categories: Vec<AnalysisCategory>,
pub regulations: Vec<crate::types::Regulation>,
pub max_session_lifetime: i64,
pub warn_no_secure: bool,
pub warn_no_httponly: bool,
pub warn_no_samesite: bool,
pub strict_mode: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum AnalysisCategory {
Security,
Compliance,
Privacy,
Performance,
BestPractices,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
security_enabled: true,
compliance_enabled: true,
tracking_enabled: true,
fingerprinting_enabled: true,
supply_chain_enabled: true,
min_severity: crate::types::Severity::Low,
categories: vec![
AnalysisCategory::Security,
AnalysisCategory::Compliance,
AnalysisCategory::Privacy,
AnalysisCategory::BestPractices,
],
regulations: vec![crate::types::Regulation::GDPR],
max_session_lifetime: 3600, warn_no_secure: true,
warn_no_httponly: true,
warn_no_samesite: true,
strict_mode: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct ReportConfig {
pub format: ReportFormat,
pub output_path: Option<String>,
pub include_summary: bool,
pub include_details: bool,
pub include_recommendations: bool,
pub include_raw_data: bool,
pub group_by_domain: bool,
pub group_by_category: bool,
pub sort_by_severity: bool,
pub max_issues: usize,
pub template_path: Option<String>,
pub title: String,
pub author: Option<String>,
pub include_timestamp: bool,
pub include_version: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ReportFormat {
Json,
Yaml,
Csv,
Html,
Pdf,
Markdown,
Text,
}
impl Default for ReportConfig {
fn default() -> Self {
Self {
format: ReportFormat::Json,
output_path: None,
include_summary: true,
include_details: true,
include_recommendations: true,
include_raw_data: false,
group_by_domain: true,
group_by_category: true,
sort_by_severity: true,
max_issues: 0,
template_path: None,
title: "ICOokForms Cookie Analysis Report".to_string(),
author: None,
include_timestamp: true,
include_version: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageConfig {
pub database_path: String,
pub cache_enabled: bool,
pub cache_ttl: u64,
pub max_cache_size: usize,
pub store_history: bool,
pub max_history: usize,
pub auto_cleanup: bool,
pub cleanup_days: u32,
}
impl Default for StorageConfig {
fn default() -> Self {
Self {
database_path: "./icookforms.db".to_string(),
cache_enabled: true,
cache_ttl: 3600,
max_cache_size: 100,
store_history: true,
max_history: 1000,
auto_cleanup: true,
cleanup_days: 30,
}
}
}
impl Config {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use = "Configuration loading failure must be handled"]
pub fn from_file(path: &str) -> crate::types::Result<Self> {
let content = std::fs::read_to_string(path)?;
if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
{
Ok(serde_json::from_str(&content)?)
} else if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
|| std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yml"))
{
Ok(serde_yaml::from_str(&content)?)
} else if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
Ok(toml::from_str(&content)
.map_err(|e| crate::types::Error::config(format!("TOML parse error: {e}")))?)
} else {
Err(crate::types::Error::config(
"Unsupported config file format",
))
}
}
pub fn save_to_file(&self, path: &str) -> crate::types::Result<()> {
let content = if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("json"))
{
serde_json::to_string_pretty(self)?
} else if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yaml"))
|| std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("yml"))
{
serde_yaml::to_string(self)?
} else if std::path::Path::new(path)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("toml"))
{
toml::to_string_pretty(self)
.map_err(|e| crate::types::Error::config(format!("TOML serialize error: {e}")))?
} else {
return Err(crate::types::Error::config(
"Unsupported config file format",
));
};
std::fs::write(path, content)?;
Ok(())
}
#[must_use = "Configuration validation result must be checked"]
pub fn validate(&self) -> crate::types::Result<()> {
if self.scan.max_pages == 0 {
return Err(crate::types::Error::config("max_pages must be > 0"));
}
if self.scan.max_depth == 0 {
return Err(crate::types::Error::config("max_depth must be > 0"));
}
if self.scan.concurrency == 0 {
return Err(crate::types::Error::config("concurrency must be > 0"));
}
if self.analysis.categories.is_empty() {
return Err(crate::types::Error::config(
"At least one analysis category must be enabled",
));
}
if self.analysis.max_session_lifetime == 0 {
return Err(crate::types::Error::config(
"max_session_lifetime must be > 0",
));
}
if self.storage.cache_ttl == 0 {
return Err(crate::types::Error::config("cache_ttl must be > 0"));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert!(config.scan.max_pages > 0);
assert!(config.analysis.security_enabled);
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validation() {
let mut config = Config::default();
config.scan.max_pages = 0;
assert!(config.validate().is_err());
}
#[test]
fn test_scan_config_defaults() {
let config = ScanConfig::default();
assert_eq!(config.max_pages, 100);
assert_eq!(config.max_depth, 3);
assert!(config.verify_ssl);
}
#[test]
fn test_analysis_config_defaults() {
let config = AnalysisConfig::default();
assert!(config.security_enabled);
assert!(config.compliance_enabled);
assert!(!config.categories.is_empty());
}
#[test]
fn test_report_format() {
let format = ReportFormat::Json;
assert_eq!(format, ReportFormat::Json);
}
}