Skip to content

Commit

Permalink
Redacts secrets from HTML report
Browse files Browse the repository at this point in the history
  • Loading branch information
jcamiel committed Dec 19, 2024
1 parent e254a39 commit 4816724
Show file tree
Hide file tree
Showing 11 changed files with 103 additions and 41 deletions.
13 changes: 10 additions & 3 deletions packages/hurl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ fn export_results(
opts: &CliOptions,
logger: &BaseLogger,
) -> Result<(), CliError> {
// Compute secrets
let secrets = opts
.secrets
.values()
.map(|v| v.as_ref())
.collect::<Vec<_>>();

if let Some(file) = &opts.curl_file {
create_curl_export(runs, file)?;
}
Expand All @@ -151,7 +158,7 @@ fn export_results(
}
if let Some(dir) = &opts.html_dir {
logger.debug(&format!("Writing HTML report to {}", dir.display()));
create_html_report(runs, dir)?;
create_html_report(runs, dir, &secrets)?;
}
if let Some(dir) = &opts.json_report_dir {
logger.debug(&format!("Writing JSON report to {}", dir.display()));
Expand Down Expand Up @@ -192,7 +199,7 @@ fn create_tap_report(runs: &[HurlRun], filename: &Path) -> Result<(), CliError>
}

/// Creates an HTML report for this run.
fn create_html_report(runs: &[HurlRun], dir_path: &Path) -> Result<(), CliError> {
fn create_html_report(runs: &[HurlRun], dir_path: &Path, secrets: &[&str]) -> Result<(), CliError> {
// We ensure that the containing folder exists.
let store_path = dir_path.join("store");
std::fs::create_dir_all(&store_path)?;
Expand All @@ -201,7 +208,7 @@ fn create_html_report(runs: &[HurlRun], dir_path: &Path) -> Result<(), CliError>
for run in runs.iter() {
let result = &run.hurl_result;
let testcase = html::Testcase::from(result, &run.filename);
testcase.write_html(&run.content, &result.entries, &store_path)?;
testcase.write_html(&run.content, &result.entries, &store_path, secrets)?;
testcases.push(testcase);
}
html::write_report(dir_path, &testcases)?;
Expand Down
10 changes: 5 additions & 5 deletions packages/hurl/src/report/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ pub use testcase::Testcase;
#[derive(Clone, Debug, PartialEq, Eq)]
struct HTMLResult {
/// Original filename, as given in the run execution
pub filename: String,
filename: String,
/// The id of the corresponding [`Testcase`]
pub id: String,
pub time_in_ms: u128,
pub success: bool,
pub timestamp: i64,
id: String,
time_in_ms: u128,
success: bool,
timestamp: i64,
}

impl HTMLResult {
Expand Down
22 changes: 17 additions & 5 deletions packages/hurl/src/report/html/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use hurl_core::error::{DisplaySourceError, OutputFormat};

use crate::report::html::Testcase;
use crate::runner::RunnerError;
use crate::util::redacted::RedactedString;

#[derive(Copy, Clone, Eq, PartialEq)]
pub enum Tab {
Expand All @@ -31,9 +32,9 @@ pub enum Tab {
impl Testcase {
/// Returns the HTML navigation component for a `tab`.
/// This common component is used to get source information and errors.
pub fn get_nav_html(&self, content: &str, tab: Tab) -> String {
pub fn get_nav_html(&self, content: &str, tab: Tab, secrets: &[&str]) -> String {
let status = get_status_html(self.success);
let errors = self.get_errors_html(content);
let errors = self.get_errors_html(content, secrets);
let errors_count = if !self.errors.is_empty() {
self.errors.len().to_string()
} else {
Expand All @@ -56,7 +57,7 @@ impl Testcase {
}

/// Formats a list of Hurl errors to HTML snippet.
fn get_errors_html(&self, content: &str) -> String {
fn get_errors_html(&self, content: &str, secrets: &[&str]) -> String {
self.errors
.iter()
.map(|(error, entry_src_info)| {
Expand All @@ -66,6 +67,7 @@ impl Testcase {
content,
&self.filename,
&self.source_filename(),
secrets,
);
format!("<div class=\"error\"><div class=\"error-desc\">{error}</div></div>")
})
Expand All @@ -89,6 +91,7 @@ fn error_to_html(
content: &str,
filename: &str,
source_filename: &str,
secrets: &[&str],
) -> String {
let line = error.source_info.start.line;
let column = error.source_info.start.column;
Expand All @@ -98,7 +101,9 @@ fn error_to_html(
Some(entry_src_info),
OutputFormat::Terminal(false),
);
let message = html_escape(&message);
let mut rs = RedactedString::new(secrets);
rs.push_str(&message);
let message = html_escape(&rs);
// We override the first part of the error string to add an anchor to
// the error context.
let old = format!("{filename}:{line}:{column}");
Expand Down Expand Up @@ -140,7 +145,14 @@ mod tests {
";
let filename = "a/b/c/foo.hurl";
let source_filename = "abc-source.hurl";
let html = error_to_html(&error, entry_src_info, content, filename, source_filename);
let html = error_to_html(
&error,
entry_src_info,
content,
filename,
source_filename,
&[],
);
assert_eq!(
html,
r##"<pre><code>Assert failure
Expand Down
2 changes: 1 addition & 1 deletion packages/hurl/src/report/html/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn write_report(dir_path: &Path, testcases: &[Testcase]) -> Result<(), Repor
let html_result = HTMLResult::from(testcase);
results.push(html_result);
}
let now: DateTime<Local> = Local::now();
let now = Local::now();
let s = create_html_index(&now.to_rfc2822(), &results);

let file_path = index_path;
Expand Down
33 changes: 22 additions & 11 deletions packages/hurl/src/report/html/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::http::Call;
use crate::report::html::nav::Tab;
use crate::report::html::Testcase;
use crate::runner::EntryResult;
use crate::util::redacted::RedactedString;

impl Testcase {
/// Creates an HTML view of a run (HTTP status code, response header etc...)
Expand All @@ -29,8 +30,9 @@ impl Testcase {
hurl_file: &HurlFile,
content: &str,
entries: &[EntryResult],
secrets: &[&str],
) -> String {
let nav = self.get_nav_html(content, Tab::Run);
let nav = self.get_nav_html(content, Tab::Run, secrets);
let nav_css = include_str!("resources/nav.css");
let run_css = include_str!("resources/run.css");

Expand All @@ -42,7 +44,7 @@ impl Testcase {
let source = self.source_filename();

run.push_str("<details open>");
let info = get_entry_html(e, entry_index + 1);
let info = get_entry_html(e, entry_index + 1, secrets);
run.push_str(&info);

for (call_index, c) in e.calls.iter().enumerate() {
Expand All @@ -53,6 +55,7 @@ impl Testcase {
&self.filename,
&source,
line,
secrets,
);
run.push_str(&info);
}
Expand All @@ -72,12 +75,12 @@ impl Testcase {
}

/// Returns an HTML view of an `entry` information as HTML (title, `entry_index` and captures).
fn get_entry_html(entry: &EntryResult, entry_index: usize) -> String {
fn get_entry_html(entry: &EntryResult, entry_index: usize, secrets: &[&str]) -> String {
let mut text = String::new();
text.push_str(&format!("<summary>Entry {entry_index}</summary>"));

let cmd = entry.curl_cmd.to_string();
let table = new_table("Debug", &[("Command", &cmd)]);
let table = new_table("Debug", &[("Command", &cmd)], secrets);
text.push_str(&table);

if !entry.captures.is_empty() {
Expand All @@ -87,7 +90,7 @@ fn get_entry_html(entry: &EntryResult, entry_index: usize) -> String {
.map(|c| (&c.name, c.value.to_string()))
.collect::<Vec<(&String, String)>>();
values.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
let table = new_table("Captures", &values);
let table = new_table("Captures", &values, secrets);
text.push_str(&table);
}

Expand All @@ -102,6 +105,7 @@ fn get_call_html(
filename: &str,
source: &str,
line: usize,
secrets: &[&str],
) -> String {
let mut text = String::new();
let id = format!("e{entry_index}:c{call_index}");
Expand All @@ -120,7 +124,7 @@ fn get_call_html(
("Status code", status.as_str()),
("Source", source.as_str()),
];
let table = new_table("General", &values);
let table = new_table("General", &values, secrets);
text.push_str(&table);

// Certificate
Expand All @@ -134,7 +138,7 @@ fn get_call_html(
("Expire Date", end_date.as_str()),
("Serial Number", certificate.serial_number.as_str()),
];
let table = new_table("Certificate", &values);
let table = new_table("Certificate", &values, secrets);
text.push_str(&table);
}

Expand All @@ -145,7 +149,7 @@ fn get_call_html(
.map(|h| (h.name.as_str(), h.value.as_str()))
.collect::<Vec<(&str, &str)>>();
values.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
let table = new_table("Request Headers", &values);
let table = new_table("Request Headers", &values, secrets);
text.push_str(&table);

let mut values = call
Expand All @@ -155,22 +159,29 @@ fn get_call_html(
.map(|h| (h.name.as_str(), h.value.as_str()))
.collect::<Vec<(&str, &str)>>();
values.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase()));
let table = new_table("Response Headers", &values);
let table = new_table("Response Headers", &values, secrets);
text.push_str(&table);

text
}

fn new_table<T: AsRef<str>, U: AsRef<str>>(title: &str, data: &[(T, U)]) -> String {
/// Returns an HTML table with a `title` and a list of key/values. Values are redacted using `secrets`.
fn new_table<T: AsRef<str>, U: AsRef<str>>(
title: &str,
data: &[(T, U)],
secrets: &[&str],
) -> String {
let mut text = String::new();
text.push_str(&format!(
"<table><thead><tr><th colspan=\"2\">{title}</tr></th></thead><tbody>"
));
data.iter().for_each(|(name, value)| {
let mut rs = RedactedString::new(secrets);
rs.push_str(value.as_ref());
text.push_str(&format!(
"<tr><td class=\"name\">{}</td><td class=\"value\">{}</td></tr>",
name.as_ref(),
value.as_ref()
rs
));
});
text.push_str("</tbody></table>");
Expand Down
4 changes: 2 additions & 2 deletions packages/hurl/src/report/html/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ use crate::runner::RunnerError;

impl Testcase {
/// Returns the HTML string of the Hurl source file (syntax colored and errors).
pub fn get_source_html(&self, hurl_file: &HurlFile, content: &str) -> String {
let nav = self.get_nav_html(content, Tab::Source);
pub fn get_source_html(&self, hurl_file: &HurlFile, content: &str, secrets: &[&str]) -> String {
let nav = self.get_nav_html(content, Tab::Source, secrets);
let nav_css = include_str!("resources/nav.css");
let source_div = hurl_core::format::format_html(hurl_file, false);
let source_div = underline_errors(&source_div, &self.errors);
Expand Down
9 changes: 6 additions & 3 deletions packages/hurl/src/report/html/testcase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,14 @@ impl Testcase {
/// - an HTML view of the Hurl source file (with potential errors and syntax colored),
/// - an HTML timeline view of the executed entries (with potential errors, waterfall)
/// - an HTML view of the executed run (headers, cookies, etc...)
///
/// `secrets` strings are redacted from teh produced HTML.
pub fn write_html(
&self,
content: &str,
entries: &[EntryResult],
dir: &Path,
secrets: &[&str],
) -> Result<(), crate::report::ReportError> {
// We parse the content as we'll reuse the AST to construct the HTML source file, and
// the waterfall.
Expand All @@ -77,19 +80,19 @@ impl Testcase {
// We create the timeline view.
let output_file = dir.join(self.timeline_filename());
let mut file = File::create(output_file)?;
let html = self.get_timeline_html(&hurl_file, content, entries);
let html = self.get_timeline_html(&hurl_file, content, entries, secrets);
file.write_all(html.as_bytes())?;

// Then create the run view.
let output_file = dir.join(self.run_filename());
let mut file = File::create(output_file)?;
let html = self.get_run_html(&hurl_file, content, entries);
let html = self.get_run_html(&hurl_file, content, entries, secrets);
file.write_all(html.as_bytes())?;

// And create the source view.
let output_file = dir.join(self.source_filename());
let mut file = File::create(output_file)?;
let html = self.get_source_html(&hurl_file, content);
let html = self.get_source_html(&hurl_file, content, secrets);
file.write_all(html.as_bytes())?;

Ok(())
Expand Down
16 changes: 13 additions & 3 deletions packages/hurl/src/report/html/timeline/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ use crate::report::html::timeline::util::{
};
use crate::report::html::timeline::{svg, CallContext, CallContextKind, CALL_HEIGHT};
use crate::report::html::Testcase;
use crate::util::redacted::RedactedString;

impl Testcase {
/// Returns a SVG view of `calls` list using contexts `call_ctxs`.
pub fn get_calls_svg(&self, calls: &[&Call], call_ctxs: &[CallContext]) -> String {
pub fn get_calls_svg(
&self,
calls: &[&Call],
call_ctxs: &[CallContext],
secrets: &[&str],
) -> String {
let margin_top = 50.px();
let margin_bottom = 250.px();

Expand Down Expand Up @@ -74,7 +80,7 @@ impl Testcase {
root.add_child(elt);

// Add calls info
let elt = new_calls(calls, call_ctxs, x, y);
let elt = new_calls(calls, call_ctxs, x, y, secrets);
root.add_child(elt);
}

Expand All @@ -91,6 +97,7 @@ fn new_calls(
call_ctxs: &[CallContext],
offset_x: Pixel,
offset_y: Pixel,
secrets: &[&str],
) -> Element {
let mut group = svg::new_group();
group.add_attr(Class("calls-list".to_string()));
Expand Down Expand Up @@ -123,7 +130,10 @@ fn new_calls(
x += 12.px();

// URL
let url = &call.request.url.to_string();
let url = call.request.url.to_string();
let mut rs = RedactedString::new(secrets);
rs.push_str(&url);
let url = &rs;
let url = url.strip_prefix("http://").unwrap_or(url);
let url = url.strip_prefix("https://").unwrap_or(url);
let text = format!("{} {url}", call.request.method);
Expand Down
7 changes: 4 additions & 3 deletions packages/hurl/src/report/html/timeline/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ impl Testcase {
hurl_file: &HurlFile,
content: &str,
entries: &[EntryResult],
secrets: &[&str],
) -> String {
let calls = entries
.iter()
Expand All @@ -69,10 +70,10 @@ impl Testcase {

let call_ctxs = self.get_call_contexts(hurl_file, entries);
let timeline_css = include_str!("../resources/timeline.css");
let nav = self.get_nav_html(content, Tab::Timeline);
let nav = self.get_nav_html(content, Tab::Timeline, secrets);
let nav_css = include_str!("../resources/nav.css");
let calls_svg = self.get_calls_svg(&calls, &call_ctxs);
let waterfall_svg = self.get_waterfall_svg(&calls, &call_ctxs);
let calls_svg = self.get_calls_svg(&calls, &call_ctxs, secrets);
let waterfall_svg = self.get_waterfall_svg(&calls, &call_ctxs, secrets);
format!(
include_str!("../resources/timeline.html"),
calls = calls_svg,
Expand Down
Loading

0 comments on commit 4816724

Please sign in to comment.