use crate::types::{AnalysisResult, Result};
use std::fmt::Write;
use std::path::Path;
pub fn generate(results: &AnalysisResult, output_path: &Path) -> Result<()> {
let csv = generate_string(results)?;
std::fs::write(output_path, csv)?;
Ok(())
}
pub fn generate_string(results: &AnalysisResult) -> Result<String> {
let mut csv = String::new();
csv.push_str(
"Type,Severity,Category,Title,Description,CookiesAffected,URLsAffected,CWEID,Confidence\n",
);
for issue in &results.issues {
writeln!(
csv,
"Issue,{},{},{},{},{},{},{},{}",
escape_csv(&issue.severity.to_string()),
escape_csv(&issue.category.to_string()),
escape_csv(&issue.title),
escape_csv(&issue.description),
issue.affected_cookies.join("; "),
issue.affected_urls.join("; "),
issue.cwe_id.as_deref().unwrap_or("N/A"),
issue.confidence
)
.unwrap();
}
for sec_issue in &results.security_issues {
writeln!(
csv,
"SecurityIssue,{},{},{},{},{},{},{},{}",
escape_csv(&sec_issue.issue.severity.to_string()),
escape_csv(&sec_issue.issue.category.to_string()),
escape_csv(&sec_issue.issue.title),
escape_csv(&sec_issue.issue.description),
sec_issue.issue.affected_cookies.join("; "),
sec_issue.issue.affected_urls.join("; "),
sec_issue.issue.cwe_id.as_deref().unwrap_or("N/A"),
sec_issue.issue.confidence
)
.unwrap();
}
for comp_issue in &results.compliance_issues {
writeln!(
csv,
"ComplianceIssue,{},{},{},{},{},{},{},{}",
escape_csv(&comp_issue.issue.severity.to_string()),
escape_csv(&comp_issue.issue.category.to_string()),
escape_csv(&comp_issue.issue.title),
escape_csv(&comp_issue.issue.description),
comp_issue.issue.affected_cookies.join("; "),
comp_issue.issue.affected_urls.join("; "),
escape_csv(&comp_issue.regulation.to_string()),
comp_issue.issue.confidence
)
.unwrap();
}
csv.push_str("\n\nTracking Cookies\n");
csv.push_str("CookieName,Type,Vendor,Purpose,RiskLevel,DataCollected\n");
for tracking in &results.tracking_info {
writeln!(
csv,
"{},{},{},{},{},{}",
escape_csv(&tracking.cookie_name),
escape_csv(&format!("{:?}", tracking.tracking_type)),
escape_csv(&tracking.vendor.clone().unwrap_or("Unknown".to_string())),
escape_csv(&tracking.purpose),
escape_csv(&tracking.risk_level.to_string()),
tracking.data_collected.join("; ")
)
.unwrap();
}
csv.push_str("\n\nSummary\n");
writeln!(csv, "Total Cookies,{}", results.summary.total_cookies).unwrap();
writeln!(csv, "First Party,{}", results.summary.first_party_cookies).unwrap();
writeln!(csv, "Third Party,{}", results.summary.third_party_cookies).unwrap();
writeln!(csv, "Session Cookies,{}", results.summary.session_cookies).unwrap();
writeln!(
csv,
"Persistent Cookies,{}",
results.summary.persistent_cookies
)
.unwrap();
writeln!(csv, "Secure Cookies,{}", results.summary.secure_cookies).unwrap();
writeln!(
csv,
"HttpOnly Cookies,{}",
results.summary.http_only_cookies
)
.unwrap();
writeln!(
csv,
"SameSite Cookies,{}",
results.summary.same_site_cookies
)
.unwrap();
writeln!(csv, "Total Issues,{}", results.summary.total_issues).unwrap();
writeln!(csv, "Risk Score,{:.2}", results.risk_score).unwrap();
writeln!(csv, "Compliance Score,{:.2}", results.compliance_score).unwrap();
writeln!(csv, "Privacy Score,{:.2}", results.privacy_score).unwrap();
Ok(csv)
}
fn escape_csv(field: &str) -> String {
if field.contains(',') || field.contains('"') || field.contains('\n') {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn test_escape_csv() {
assert_eq!(escape_csv("simple"), "simple");
assert_eq!(escape_csv("with,comma"), "\"with,comma\"");
assert_eq!(escape_csv("with\"quote"), "\"with\"\"quote\"");
}
#[test]
fn test_generate_string() {
let results = AnalysisResult::new("test-scan");
let csv = generate_string(&results);
assert!(csv.is_ok());
let content = csv.unwrap();
assert!(content.contains("Type,Severity,Category"));
assert!(content.contains("Summary"));
}
#[test]
fn test_generate_file() {
let results = AnalysisResult::new("test-scan");
let temp_file = NamedTempFile::new().unwrap();
let result = generate(&results, temp_file.path());
assert!(result.is_ok());
let content = std::fs::read_to_string(temp_file.path()).unwrap();
assert!(content.contains("Type,Severity"));
}
}