use std::io::Write;
use crate::VerifiedFinding;
use super::{impl_writer_backed, HtmlScanMetadata, ReportError, Reporter, WriterBackedReporter};
fn escape_for_script(serialized: &str) -> String {
let mut out = String::with_capacity(serialized.len());
for ch in serialized.chars() {
match ch {
'<' => out.push_str("\\u003c"),
'>' => out.push_str("\\u003e"),
'/' => out.push_str("\\u002f"),
'\u{2028}' => out.push_str("\\u2028"),
'\u{2029}' => out.push_str("\\u2029"),
other => out.push(other),
}
}
out
}
pub(crate) struct HtmlReporter<W: Write + Send> {
writer: W,
findings: Vec<VerifiedFinding>,
skip_summary: Vec<(String, usize)>,
metadata: Option<HtmlScanMetadata>,
}
impl<W: Write + Send> HtmlReporter<W> {
pub(crate) fn new(writer: W) -> Self {
Self {
writer,
findings: Vec::new(),
skip_summary: Vec::new(),
metadata: None,
}
}
pub(crate) fn with_skip_summary(mut self, skip_summary: Vec<(String, usize)>) -> Self {
self.skip_summary = skip_summary.into_iter().filter(|(_, n)| *n > 0).collect();
self
}
pub(crate) fn with_metadata(mut self, metadata: Option<HtmlScanMetadata>) -> Self {
self.metadata = metadata;
self
}
}
impl<W: Write + Send> Reporter for HtmlReporter<W> {
fn report(&mut self, finding: &VerifiedFinding) -> Result<(), ReportError> {
self.findings.push(finding.clone());
Ok(())
}
fn finish(&mut self) -> Result<(), ReportError> {
let mut findings_value = serde_json::to_value(&self.findings)?;
if let Some(arr) = findings_value.as_array_mut() {
for finding in arr {
if let Some(verification) = finding.get_mut("verification") {
if verification
.as_object()
.is_some_and(|object| object.contains_key("error"))
{
*verification = serde_json::Value::String("error".to_string());
}
}
}
}
let serialized_findings = escape_for_script(&serde_json::to_string(&findings_value)?);
let coverage_value: Vec<serde_json::Value> = self
.skip_summary
.iter()
.map(|(reason, count)| serde_json::json!({ "reason": reason, "count": count }))
.collect();
let serialized_coverage = escape_for_script(&serde_json::to_string(&coverage_value)?);
let serialized_metadata = escape_for_script(&serde_json::to_string(&self.metadata)?);
writeln!(self.writer, "<!DOCTYPE html>")?;
writeln!(self.writer, "<html lang=\"en\" data-theme=\"keyhog\">")?;
writeln!(self.writer, "<head>")?;
writeln!(self.writer, " <meta charset=\"UTF-8\">")?;
writeln!(
self.writer,
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
)?;
writeln!(self.writer, " <title>KeyHog Secret Scan Report</title>")?;
writeln!(self.writer, " <style>")?;
writeln!(self.writer, "{}", include_str!("html_styles.css"))?;
writeln!(self.writer, " </style>")?;
writeln!(self.writer, "</head>")?;
writeln!(self.writer, "<body>")?;
writeln!(self.writer, "{}", include_str!("html_body.html"))?;
writeln!(self.writer, " <script>")?;
writeln!(
self.writer,
" const rawFindings = {};",
serialized_findings
)?;
writeln!(
self.writer,
" const coverageGaps = {};",
serialized_coverage
)?;
writeln!(
self.writer,
" const scanMetadata = {};",
serialized_metadata
)?;
writeln!(self.writer, "{}", include_str!("html_script.js"))?;
writeln!(self.writer, " </script>")?;
writeln!(self.writer, "</body>")?;
writeln!(self.writer, "</html>")?;
self.flush_writer()
}
}
impl_writer_backed!(HtmlReporter);