diff --git a/addOns/reports/CHANGELOG.md b/addOns/reports/CHANGELOG.md index a11373f2c46..64108b7337b 100644 --- a/addOns/reports/CHANGELOG.md +++ b/addOns/reports/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Help content related to the params add-on was updated (Issue 9210). +- Generation of reports now supports producing a ZIP containing the report file(s) (Issue 7821). ## [0.45.0] - 2026-05-06 ### Fixed diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ExtensionReports.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ExtensionReports.java index aa1736ceafd..ebfef41698d 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ExtensionReports.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ExtensionReports.java @@ -21,17 +21,21 @@ import com.lowagie.text.DocumentException; import java.awt.Desktop; +import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; @@ -45,6 +49,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.tree.DefaultTreeModel; @@ -419,56 +425,61 @@ public File generateReport( // Handle any resources File resourcesDir = template.getResourcesDir(); + String resourcesEntryName = null; if (resourcesDir.exists()) { - String subDirName; - int dotIndex = reportFilename.lastIndexOf("."); - if (dotIndex > 0) { - subDirName = reportFilename.substring(0, dotIndex); - } else { - subDirName = reportFilename + "_d"; - } - File subDir = new File(subDirName); - int i = 1; - while (subDir.exists()) { - i += 1; - subDir = new File(subDirName + i); + resourcesEntryName = getResourcesEntryName(reportFilename); + context.setVariable("resources", resourcesEntryName); + if (!reportData.isZipReport()) { + File resourcesSubDir = getResourcesSubDir(reportFilename); + LOGGER.debug( + "Copying resources from {} to {}", + resourcesDir.getAbsolutePath(), + resourcesSubDir.getAbsolutePath()); + FileUtils.copyDirectory(resourcesDir, resourcesSubDir); } - LOGGER.debug( - "Copying resources from {} to {}", - resourcesDir.getAbsolutePath(), - subDir.getAbsolutePath()); - FileUtils.copyDirectory(resourcesDir, subDir); - context.setVariable("resources", subDir.getName()); } - File file = new File(reportFilename); - try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { - templateEngine.process( - template.getReportTemplateFile().getAbsolutePath(), context, writer); - Stats.incCounter("stats.reports.generated." + template.getConfigName()); - } + File file; + if (reportData.isZipReport()) { + file = + generateZippedReport( + templateEngine, + template, + context, + reportFilename, + resourcesDir, + resourcesEntryName); + } else { + file = new File(reportFilename); + try (Writer writer = + Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + templateEngine.process( + template.getReportTemplateFile().getAbsolutePath(), context, writer); + Stats.incCounter("stats.reports.generated." + template.getConfigName()); + } - if ("PDF".equals(template.getFormat())) { - // Will have appended ".html" above - reportFilename = reportFilename.substring(0, reportFilename.length() - 5); - reportFilename += ".pdf"; - File pdfFile = new File(reportFilename); - try (OutputStream outputStream = new FileOutputStream(pdfFile)) { - ITextRenderer renderer = new ITextRenderer(file); - renderer.layout(); - try { - renderer.createPDF(outputStream); - } catch (DocumentException e) { - // Throw a standard exception so that add-ons using this method don't need - // to - // import it - throw new IOException("Invalid template: " + template.getConfigName(), e); + if ("PDF".equals(template.getFormat())) { + // Will have appended ".html" above + reportFilename = reportFilename.substring(0, reportFilename.length() - 5); + reportFilename += ".pdf"; + File pdfFile = new File(reportFilename); + try (OutputStream outputStream = new FileOutputStream(pdfFile)) { + ITextRenderer renderer = new ITextRenderer(file); + renderer.layout(); + try { + renderer.createPDF(outputStream); + } catch (DocumentException e) { + // Throw a standard exception so that add-ons using this method don't + // need to import it + throw new IOException( + "Invalid template: " + template.getConfigName(), e); + } } + if (!file.delete()) { + LOGGER.debug("Failed to delete interim report {}", file.getAbsolutePath()); + } + file = pdfFile; } - if (!file.delete()) { - LOGGER.debug("Failed to delete interim report {}", file.getAbsolutePath()); - } - file = pdfFile; } LOGGER.debug("Generated report {}", file.getAbsolutePath()); @@ -476,8 +487,7 @@ public File generateReport( if ("HTML".equals(template.getFormat())) { DesktopUtils.openUrlInBrowser(file.toURI()); } else { - Desktop desktop = Desktop.getDesktop(); - desktop.open(file); + Desktop.getDesktop().open(file); } } return file; @@ -502,6 +512,151 @@ public File generateReport( } } + private File generateZippedReport( + TemplateEngine templateEngine, + Template template, + Context context, + String reportFilename, + File resourcesDir, + String resourcesEntryName) + throws IOException { + File zipFile = new File(getZipFilePath(getFinalReportPath(reportFilename, template))); + + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { + if (resourcesEntryName != null) { + addDirectoryToZip(zos, resourcesDir, resourcesEntryName + "/"); + } + + if ("PDF".equals(template.getFormat())) { + addPdfReportToZip( + zos, + templateEngine, + template, + context, + getReportEntryName(getFinalReportPath(reportFilename, template))); + } else { + addGeneratedReportToZip( + zos, templateEngine, template, context, getReportEntryName(reportFilename)); + } + } + Stats.incCounter("stats.reports.generated." + template.getConfigName()); + Stats.incCounter("stats.reports.zipped"); + return zipFile; + } + + private void addGeneratedReportToZip( + ZipOutputStream zos, + TemplateEngine templateEngine, + Template template, + Context context, + String entryName) + throws IOException { + ByteArrayOutputStream reportContent = new ByteArrayOutputStream(); + try (Writer writer = new OutputStreamWriter(reportContent, StandardCharsets.UTF_8)) { + templateEngine.process( + template.getReportTemplateFile().getAbsolutePath(), context, writer); + } + addBytesToZip(zos, reportContent.toByteArray(), entryName); + } + + private void addPdfReportToZip( + ZipOutputStream zos, + TemplateEngine templateEngine, + Template template, + Context context, + String entryName) + throws IOException { + Path tempHtml = Files.createTempFile("zap-report-", ".html"); + try { + try (Writer writer = Files.newBufferedWriter(tempHtml, StandardCharsets.UTF_8)) { + templateEngine.process( + template.getReportTemplateFile().getAbsolutePath(), context, writer); + } + ByteArrayOutputStream pdfContent = new ByteArrayOutputStream(); + ITextRenderer renderer = new ITextRenderer(tempHtml.toFile()); + renderer.layout(); + try { + renderer.createPDF(pdfContent); + } catch (DocumentException e) { + throw new IOException("Invalid template: " + template.getConfigName(), e); + } + addBytesToZip(zos, pdfContent.toByteArray(), entryName); + } finally { + Files.deleteIfExists(tempHtml); + } + } + + private static String getResourcesEntryName(String reportFilename) { + String fileName = Paths.get(reportFilename).getFileName().toString(); + int dotIndex = fileName.lastIndexOf('.'); + if (dotIndex > 0) { + return fileName.substring(0, dotIndex); + } + return fileName + "_d"; + } + + private static File getResourcesSubDir(String reportFilename) { + Path parent = Paths.get(reportFilename).getParent(); + String resourcesEntryName = getResourcesEntryName(reportFilename); + if (parent == null) { + return new File(resourcesEntryName); + } + return parent.resolve(resourcesEntryName).toFile(); + } + + private static String getFinalReportPath(String reportFilename, Template template) { + if (!"PDF".equals(template.getFormat())) { + return reportFilename; + } + if (reportFilename.toLowerCase().endsWith(".html")) { + return reportFilename.substring(0, reportFilename.length() - 5) + ".pdf"; + } + return reportFilename; + } + + private static String getReportEntryName(String reportFilename) { + return Paths.get(reportFilename).getFileName().toString(); + } + + private String getZipFilePath(String reportFilename) { + int dotIndex = reportFilename.lastIndexOf('.'); + if (dotIndex > 0) { + return reportFilename.substring(0, dotIndex) + ".zip"; + } + return reportFilename + ".zip"; + } + + private void addBytesToZip(ZipOutputStream zos, byte[] data, String entryName) + throws IOException { + zos.putNextEntry(new ZipEntry(entryName)); + zos.write(data); + zos.closeEntry(); + } + + private void addFileToZip(ZipOutputStream zos, File file, String entryName) throws IOException { + try (var inputStream = Files.newInputStream(file.toPath())) { + zos.putNextEntry(new ZipEntry(entryName)); + inputStream.transferTo(zos); + zos.closeEntry(); + } + } + + private void addDirectoryToZip(ZipOutputStream zos, File dir, String basePath) + throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + String entryName = basePath + file.getName(); + if (file.isDirectory()) { + addDirectoryToZip(zos, file, entryName + "/"); + } else { + addFileToZip(zos, file, entryName); + } + } + } + /** * Set (add) a class which can be used to add more data to reports. * diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportApi.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportApi.java index 8dfb3bd07f3..1a86f14728c 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportApi.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportApi.java @@ -73,6 +73,7 @@ public class ReportApi extends ApiImplementor { static final String PARAM_THEME = "theme"; static final String PARAM_TITLE = "title"; static final String PARAM_DISPLAY = "display"; + static final String PARAM_ZIP = "zip"; static final String PARAM_INC_CONFIDENCES = "includedConfidences"; static final String PARAM_INC_RISKS = "includedRisks"; static final String PARAM_REPORT_FILE_NAME_PATTERN = "reportFileNamePattern"; @@ -106,6 +107,7 @@ public ReportApi(ExtensionReports extReports) { PARAM_REPORT_FILE_NAME_PATTERN, PARAM_REPORT_DIRECTORY, PARAM_DISPLAY, + PARAM_ZIP, })); this.addApiView(new ApiView(VIEW_TEMPLATES)); this.addApiView(new ApiView(VIEW_TEMPLATE_DETAILS, new String[] {PARAM_TEMPLATE})); @@ -252,14 +254,21 @@ public ApiResponse handleApiAction(String name, JSONObject params) throws ApiExc } String reportFilePath = Paths.get(paramReportDir, reportFileName).toString(); - boolean display = params.optBoolean(PARAM_DISPLAY, false); + boolean zipReport = params.optBoolean(PARAM_ZIP, false); + boolean display = + ReportOutputOptions.resolveDisplay( + zipReport, params.optBoolean(PARAM_DISPLAY, false)); + reportData.setZipReport(zipReport); try { - extReports.generateReport(reportData, template, reportFilePath, display); + return new ApiResponseElement( + name, + extReports + .generateReport(reportData, template, reportFilePath, display) + .getAbsolutePath()); } catch (Exception e) { throw new ApiException(Type.INTERNAL_ERROR, e); } - return new ApiResponseElement(name, reportFilePath); default: throw new ApiException(Type.BAD_ACTION); } diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportData.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportData.java index 40e4df123f6..010d4b9cb5e 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportData.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportData.java @@ -24,6 +24,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import lombok.Getter; +import lombok.Setter; import org.parosproxy.paros.core.scanner.Alert; import org.zaproxy.zap.extension.alert.AlertNode; import org.zaproxy.zap.model.Context; @@ -42,6 +44,8 @@ public class ReportData { private List sections = new ArrayList<>(); private String theme; + @Getter @Setter private boolean zipReport; + @Deprecated public ReportData() {} diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportDialog.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportDialog.java index 8c53db03800..6749e790f61 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportDialog.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportDialog.java @@ -72,6 +72,7 @@ public class ReportDialog extends StandardFieldsDialog { private static final String FIELD_REPORT_NAME_PATTERN = "reports.dialog.field.namepattern"; private static final String FIELD_CONFIDENCE_HEADER = "reports.dialog.field.confidence"; private static final String FIELD_CONFIDENCE_0 = "reports.dialog.field.confidence.0"; + private static final String FIELD_ZIP_REPORT = "reports.dialog.field.zip"; private static final String FIELD_CONFIDENCE_1 = "reports.dialog.field.confidence.1"; private static final String FIELD_CONFIDENCE_2 = "reports.dialog.field.confidence.2"; private static final String FIELD_CONFIDENCE_3 = "reports.dialog.field.confidence.3"; @@ -230,7 +231,13 @@ public void changedUpdate(DocumentEvent e) { this.addCustomComponent( TAB_SCOPE, FIELD_SITES, getNewJScrollPane(getSitesSelector(), 400, 100)); this.addCheckBoxField(TAB_SCOPE, FIELD_GENERATE_ANYWAY, false); - this.addCheckBoxField(TAB_SCOPE, FIELD_DISPLAY_REPORT, reportParam.isDisplayReport()); + boolean[] zipAndDisplay = + ReportOutputOptions.resolveInitialSelection( + reportParam.isZipReport(), reportParam.isDisplayReport()); + this.addCheckBoxField(TAB_SCOPE, FIELD_ZIP_REPORT, zipAndDisplay[0]); + this.addCheckBoxField(TAB_SCOPE, FIELD_DISPLAY_REPORT, zipAndDisplay[1]); + ReportOutputOptions.bindMutuallyExclusiveCheckBoxes( + this, FIELD_ZIP_REPORT, FIELD_DISPLAY_REPORT); Template defaultTemplate = extension.getTemplateByConfigName(reportParam.getTemplate()); List templates = extension.getTemplateNames(); @@ -457,6 +464,8 @@ private ReportData getReportData(Template template) { } } + reportData.setZipReport(this.getBoolValue(FIELD_ZIP_REPORT)); + // Always do this last as it depends on the other fields reportData.setAlertTreeRootNode(extension.getFilteredAlertTree(reportData)); return reportData; @@ -464,12 +473,16 @@ private ReportData getReportData(Template template) { @Override public void save() { - boolean displayReport = this.getBoolValue(FIELD_DISPLAY_REPORT); + boolean zipReport = this.getBoolValue(FIELD_ZIP_REPORT); + boolean displayReport = + ReportOutputOptions.resolveDisplay( + zipReport, this.getBoolValue(FIELD_DISPLAY_REPORT)); Template template = extension.getTemplateByDisplayName(getStringValue(FIELD_TEMPLATE)); ReportData reportData = getReportData(template); // Always save all of the options ReportParam reportParam = extension.getReportParam(); + reportParam.setZipReport(zipReport); reportParam.setDisplayReport(displayReport); reportParam.setTitle(reportData.getTitle()); reportParam.setDescription(reportData.getDescription()); diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportOutputOptions.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportOutputOptions.java new file mode 100644 index 00000000000..69e9ebaee25 --- /dev/null +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportOutputOptions.java @@ -0,0 +1,64 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2026 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.reports; + +import java.awt.event.ItemEvent; +import javax.swing.JCheckBox; +import org.zaproxy.zap.view.StandardFieldsDialog; + +/** ZIP and display report output options are mutually exclusive. */ +public final class ReportOutputOptions { + + private ReportOutputOptions() {} + + public static boolean resolveDisplay(boolean zipReport, boolean displayReport) { + return zipReport ? false : displayReport; + } + + public static boolean bothEnabled(boolean zipReport, boolean displayReport) { + return zipReport && displayReport; + } + + public static boolean[] resolveInitialSelection(boolean zipReport, boolean displayReport) { + if (zipReport && displayReport) { + return new boolean[] {true, false}; + } + return new boolean[] {zipReport, displayReport}; + } + + public static void bindMutuallyExclusiveCheckBoxes( + StandardFieldsDialog dialog, String zipFieldKey, String displayFieldKey) { + JCheckBox zipCheckBox = (JCheckBox) dialog.getField(zipFieldKey); + JCheckBox displayCheckBox = (JCheckBox) dialog.getField(displayFieldKey); + + zipCheckBox.addItemListener( + e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + displayCheckBox.setSelected(false); + } + }); + displayCheckBox.addItemListener( + e -> { + if (e.getStateChange() == ItemEvent.SELECTED) { + zipCheckBox.setSelected(false); + } + }); + } +} diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportParam.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportParam.java index 6b0ba448dca..07397c6db12 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportParam.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/ReportParam.java @@ -35,6 +35,7 @@ public class ReportParam extends AbstractParam { private static final String PARAM_DESCRIPTION = PARAM_BASE_KEY + ".description"; private static final String PARAM_TEMPLATE = PARAM_BASE_KEY + ".template"; private static final String PARAM_DISPLAY = PARAM_BASE_KEY + ".display"; + private static final String PARAM_ZIP = PARAM_BASE_KEY + ".zip"; private static final String PARAM_TEMPLATE_DIRECTORY = PARAM_BASE_KEY + ".templateDir"; private static final String PARAM_REPORT_DIRECTORY = PARAM_BASE_KEY + ".reportDir"; private static final String PARAM_REPORT_NAME_PATTERN = PARAM_BASE_KEY + ".reportPattern"; @@ -61,6 +62,7 @@ public class ReportParam extends AbstractParam { private String reportDirectory; private String reportNamePattern; private boolean displayReport; + private boolean zipReport; private boolean incConfidence0; private boolean incConfidence1; private boolean incConfidence2; @@ -89,6 +91,7 @@ protected void parse() { reportDirectory = getString(PARAM_REPORT_DIRECTORY, System.getProperty("user.home")); reportNamePattern = getString(PARAM_REPORT_NAME_PATTERN, DEFAULT_NAME_PATTERN); displayReport = getBoolean(PARAM_DISPLAY, true); + zipReport = getBoolean(PARAM_ZIP, false); incConfidence0 = getBoolean(PARAM_INC_CONFIDENCE_0, false); incConfidence1 = getBoolean(PARAM_INC_CONFIDENCE_1, true); @@ -165,6 +168,15 @@ public void setDisplayReport(boolean displayReport) { getConfig().setProperty(PARAM_DISPLAY, displayReport); } + public boolean isZipReport() { + return zipReport; + } + + public void setZipReport(boolean zipReport) { + this.zipReport = zipReport; + getConfig().setProperty(PARAM_ZIP, zipReport); + } + public boolean isIncConfidence0() { return incConfidence0; } diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJob.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJob.java index e7546d07eb3..6badc69cc59 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJob.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJob.java @@ -40,6 +40,7 @@ import org.zaproxy.addon.automation.jobs.JobUtils; import org.zaproxy.addon.reports.ExtensionReports; import org.zaproxy.addon.reports.ReportData; +import org.zaproxy.addon.reports.ReportOutputOptions; import org.zaproxy.addon.reports.ReportParam; import org.zaproxy.addon.reports.Template; @@ -56,6 +57,7 @@ public class ReportJob extends AutomationJob { private static final String PARAM_REPORT_TITLE = "reportTitle"; private static final String PARAM_REPORT_DESC = "reportDescription"; private static final String PARAM_DISPLAY_REPORT = "displayReport"; + private static final String PARAM_ZIP_REPORT = "zipReport"; private ExtensionReports extReport; @@ -259,6 +261,16 @@ public void runJob(AutomationEnvironment env, AutomationProgress progress) { } reportData.setAlertTreeRootNode(getExtReport().getFilteredAlertTree(reportData)); + boolean zipReport = JobUtils.unBox(this.getParameters().getZipReport()); + boolean displayReport = JobUtils.unBox(this.getParameters().getDisplayReport()); + if (ReportOutputOptions.bothEnabled(zipReport, displayReport)) { + String message = + Constant.messages.getString( + "reports.automation.warn.zipanddisplay", this.getName()); + progress.warn(message); + LOGGER.warn(message); + } + reportData.setZipReport(zipReport); try { file = @@ -267,7 +279,7 @@ public void runJob(AutomationEnvironment env, AutomationProgress progress) { reportData, template, file.getAbsolutePath(), - JobUtils.unBox(this.getParameters().getDisplayReport())); + ReportOutputOptions.resolveDisplay(zipReport, displayReport)); progress.info( Constant.messages.getString( "reports.automation.info.reportgen", @@ -398,6 +410,9 @@ public Map getCustomConfigParameters() { map.put( PARAM_DISPLAY_REPORT, Boolean.toString(JobUtils.unBox(this.getParameters().getDisplayReport()))); + map.put( + PARAM_ZIP_REPORT, + Boolean.toString(JobUtils.unBox(this.getParameters().getZipReport()))); return map; } @@ -477,5 +492,6 @@ public static class Parameters extends AutomationData { private String reportTitle = ""; private String reportDescription = ""; private Boolean displayReport = false; + private Boolean zipReport = false; } } diff --git a/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJobDialog.java b/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJobDialog.java index efb8a316446..14ad8c68f57 100644 --- a/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJobDialog.java +++ b/addOns/reports/src/main/java/org/zaproxy/addon/reports/automation/ReportJobDialog.java @@ -46,6 +46,7 @@ import org.zaproxy.addon.automation.jobs.JobUtils; import org.zaproxy.addon.reports.ExtensionReports; import org.zaproxy.addon.reports.ReflectionUtils; +import org.zaproxy.addon.reports.ReportOutputOptions; import org.zaproxy.addon.reports.Template; import org.zaproxy.addon.reports.automation.ReportJob.Parameters; import org.zaproxy.zap.utils.DisplayUtils; @@ -61,6 +62,7 @@ public class ReportJobDialog extends StandardFieldsDialog { private static final String FIELD_REPORT_NAME = "reports.dialog.field.reportname"; private static final String FIELD_DESCRIPTION = "reports.dialog.field.description"; private static final String FIELD_DISPLAY_REPORT = "reports.dialog.field.display"; + private static final String FIELD_ZIP_REPORT = "reports.automation.dialog.field.zip"; private static final String FIELD_CONFIDENCE_HEADER = "reports.dialog.field.confidence"; private static final String FIELD_CONFIDENCE_0 = "reports.dialog.field.confidence.0"; private static final String FIELD_CONFIDENCE_1 = "reports.dialog.field.confidence.1"; @@ -120,8 +122,14 @@ public ReportJobDialog(ReportJob job) { this.addMultilineField(TAB_SCOPE, FIELD_DESCRIPTION, params.getReportDescription()); this.addMultilineField(TAB_SCOPE, FIELD_SITES, listToString(job.getData().getSites())); - this.addCheckBoxField( - TAB_SCOPE, FIELD_DISPLAY_REPORT, JobUtils.unBox(params.getDisplayReport())); + boolean[] zipAndDisplay = + ReportOutputOptions.resolveInitialSelection( + JobUtils.unBox(params.getZipReport()), + JobUtils.unBox(params.getDisplayReport())); + this.addCheckBoxField(TAB_SCOPE, FIELD_ZIP_REPORT, zipAndDisplay[0]); + this.addCheckBoxField(TAB_SCOPE, FIELD_DISPLAY_REPORT, zipAndDisplay[1]); + ReportOutputOptions.bindMutuallyExclusiveCheckBoxes( + this, FIELD_ZIP_REPORT, FIELD_DISPLAY_REPORT); Template defaultTemplate = extension.getTemplateByConfigName(params.getTemplate()); List templates = extension.getTemplateNames(); @@ -280,7 +288,13 @@ public void save() { job.getData().getParameters().setReportFile(this.getStringValue(FIELD_REPORT_NAME)); job.getData().getParameters().setReportDir(this.getStringValue(FIELD_REPORT_DIR)); job.getData().getParameters().setReportDescription(this.getStringValue(FIELD_DESCRIPTION)); - job.getData().getParameters().setDisplayReport(this.getBoolValue(FIELD_DISPLAY_REPORT)); + boolean zipReport = this.getBoolValue(FIELD_ZIP_REPORT); + job.getData().getParameters().setZipReport(zipReport); + job.getData() + .getParameters() + .setDisplayReport( + ReportOutputOptions.resolveDisplay( + zipReport, this.getBoolValue(FIELD_DISPLAY_REPORT))); job.getData() .getParameters() .setTheme(template.getThemeForName(this.getStringValue(FIELD_THEME))); diff --git a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/api.html b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/api.html index ead3403318a..c7ad95c2c38 100644 --- a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/api.html +++ b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/api.html @@ -19,8 +19,8 @@

Actions

  • generate (title* template* theme description contexts sites sections includedConfidences includedRisks reportFileName - reportFileNamePattern reportDir display): Generate a report with the - supplied parameters. + reportFileNamePattern reportDir display zip): Generate a report with + the supplied parameters.
    • title: Report Title
    • template: Report Template
    • @@ -49,7 +49,12 @@

      Actions

    • reportDir: Path to directory in which the generated report should be placed.
    • display: Display the generated report. Either "true" or - "false".
    • + "false". Cannot be used together with zip. +
    • zip: ZIP the report output. Either "true" or "false". When + "true", the generated report and any template resources are + packaged into a single .zip file. Cannot be used + together with display. +
diff --git a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/automation.html b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/automation.html index 1c8057b61cf..c78d82a50ef 100644 --- a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/automation.html +++ b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/automation.html @@ -21,6 +21,7 @@

Job: report

reportTitle: # String: The report title reportDescription: # String: The report description displayReport: # Boolean: Display the report when generated, default: false + zipReport: true # Boolean: ZIP the report output, default: false (mutually exclusive with displayReport) risks: # List: The risks to include in this report, default all - high - medium diff --git a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/reports.html b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/reports.html index 5bdcc8a5962..20e15311e51 100644 --- a/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/reports.html +++ b/addOns/reports/src/main/javahelp/org/zaproxy/addon/reports/resources/help/contents/reports.html @@ -63,9 +63,19 @@

Generate If No Alerts

alerts. Select this check box if you do want to generate a report with no alerts. +

ZIP Report

+ If selected then the generated report file and any template resources + (such as CSS, JavaScript, and images) will be written directly into a + single + .zip + file in the report directory. +

This option is saved with the other report options. It cannot be + selected at the same time as Display Report.

+

Display Report

If selected then ZAP will attempt to open the report using the default - program used by your operating system for that report type. + program used by your operating system for that report type. It cannot + be selected at the same time as ZIP Report.

Template Fields

The Template tab has the following fields: diff --git a/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/Messages.properties b/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/Messages.properties index 0d44326c2cf..c5e85aeb763 100644 --- a/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/Messages.properties +++ b/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/Messages.properties @@ -12,6 +12,7 @@ reports.api.action.generate.param.sites = The site URLs that should be included reports.api.action.generate.param.template = Report Template reports.api.action.generate.param.theme = Report Theme reports.api.action.generate.param.title = Report Title +reports.api.action.generate.param.zip = ZIP the report. Either "true" or "false". reports.api.error.badSections = Invalid sections {0} for template {1} reports.api.error.badTheme = Invalid theme {0} for template {1} @@ -23,6 +24,7 @@ reports.api.view.templates = View available templates. reports.automation.desc = Report Generation Automation Integration reports.automation.dialog.field.name = Job Name: +reports.automation.dialog.field.zip = ZIP Report: reports.automation.dialog.summary = Template: {0} reports.automation.dialog.title = Report Job reports.automation.error.badconf = Job {0} invalid confidence: {1} @@ -37,6 +39,7 @@ reports.automation.error.noparent = Job {0} parent directory of summaryFile does reports.automation.error.roparent = Job {0} no write access to parent directory of summaryFile {1} reports.automation.info.reportgen = Job {0} generated report {1} reports.automation.name = Report Generation Automation Integration +reports.automation.warn.zipanddisplay = Job {0} has both zipReport and displayReport enabled; displayReport will be ignored. reports.desc = Templated and themed report generation functionality @@ -77,6 +80,7 @@ reports.dialog.field.template = Template: reports.dialog.field.templatedir = Template Directory: reports.dialog.field.theme = Theme: reports.dialog.field.title = Report Title: +reports.dialog.field.zip = ZIP Report: reports.dialog.info.reloadtemplates = Loaded {0} templates from {1} diff --git a/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/report-max.yaml b/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/report-max.yaml index ffcff9bfc03..326a2d380e2 100644 --- a/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/report-max.yaml +++ b/addOns/reports/src/main/resources/org/zaproxy/addon/reports/resources/report-max.yaml @@ -7,6 +7,7 @@ reportTitle: # String: The report title reportDescription: # String: The report description displayReport: # Boolean: Display the report when generated, default: false + zipReport: true # Boolean: ZIP the report output, default: false (mutually exclusive with displayReport) risks: # List: The risks to include in this report, default all - high - medium diff --git a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ExtensionReportsUnitTest.java b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ExtensionReportsUnitTest.java index d56b2934669..feaad2a2a17 100644 --- a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ExtensionReportsUnitTest.java +++ b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ExtensionReportsUnitTest.java @@ -20,6 +20,8 @@ package org.zaproxy.addon.reports; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThan; @@ -32,9 +34,11 @@ import static org.mockito.Mockito.withSettings; import java.io.File; +import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -43,6 +47,11 @@ import java.util.Locale; import java.util.Map; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LoggerContext; @@ -52,10 +61,13 @@ import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.config.Property; import org.apache.logging.log4j.core.layout.PatternLayout; +import org.hamcrest.Matcher; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.quality.Strictness; import org.parosproxy.paros.Constant; @@ -818,6 +830,167 @@ void shouldGenerateReportsWithoutWarnings(String reportName) throws Exception { assertThat(logEvents, not(hasItem(startsWith("ERROR")))); } + @Test + void shouldGenerateZippedHtmlReportWithResources() throws Exception { + withZipWorkDir( + workDir -> { + // Given / When + File result = + generateReport(workDir, "traditional-html-plus", "report.html", true); + + // Then + assertZippedReport(result, workDir, "report.html"); + assertThat(result.getParentFile().toPath(), is(equalTo(workDir))); + assertThat(Files.exists(workDir.resolve("report")), is(false)); + assertZipEntries( + result, allOf(hasItem("report.html"), hasItem("report/common.css"))); + }); + } + + @Test + void shouldNotZipWhenDisabled() throws Exception { + withZipWorkDir( + workDir -> { + // Given / When + File result = + generateReport(workDir, "traditional-html-plus", "report.html", false); + + // Then + assertNotZipped(result, workDir, "report.html"); + }); + } + + @ParameterizedTest + @MethodSource("zippedSingleFileReportCases") + void shouldGenerateZippedReportWithSingleFile( + String templateName, String reportFileName, boolean withAlerts) throws Exception { + withZipWorkDir( + workDir -> { + // Given / When + File result = + withAlerts + ? generateReport( + workDir, + templateName, + reportFileName, + true, + setupReportData()) + : generateReport(workDir, templateName, reportFileName, true); + + // Then + assertZippedReport(result, workDir, reportFileName); + assertZipEntries(result, containsInAnyOrder(reportFileName)); + }); + } + + static Stream zippedSingleFileReportCases() { + return Stream.of( + Arguments.of("traditional-md", "report.md", false), + Arguments.of("traditional-pdf", "report.pdf", true)); + } + + @Test + void shouldIgnoreStaleResourcesDirWhenWritingZip() throws Exception { + withZipWorkDir( + workDir -> { + // Given + Path staleResourcesDir = workDir.resolve("report"); + Files.createDirectories(staleResourcesDir); + Files.writeString(staleResourcesDir.resolve("stale.txt"), "stale"); + + // When + File result = + generateReport(workDir, "traditional-html-plus", "report.html", true); + + // Then + assertThat(Files.exists(workDir.resolve("report.html")), is(false)); + assertThat(Files.exists(workDir.resolve("report")), is(true)); + assertZipEntries( + result, + allOf(hasItem("report/common.css"), not(hasItem("report/stale.txt")))); + }); + } + + @FunctionalInterface + private interface ZipWorkDirConsumer { + void accept(Path workDir) throws Exception; + } + + private static void withZipWorkDir(ZipWorkDirConsumer consumer) throws Exception { + Path workDir = Files.createTempDirectory("zap-reports-zip-test"); + try { + consumer.accept(workDir); + } finally { + deleteDirectory(workDir); + } + } + + private static File generateReport( + Path workDir, String templateName, String reportFileName, boolean zipReport) + throws Exception { + return generateReport( + workDir, + templateName, + reportFileName, + zipReport, + ReportTestUtils.getTestReportData()); + } + + private static File generateReport( + Path workDir, + String templateName, + String reportFileName, + boolean zipReport, + ReportData reportData) + throws Exception { + ExtensionReports extRep = new ExtensionReports(); + Template template = ReportTestUtils.getTemplateFromYamlFile(templateName); + reportData.setSections(template.getSections()); + reportData.setZipReport(zipReport); + return extRep.generateReport( + reportData, + template, + workDir.resolve(reportFileName).toAbsolutePath().toString(), + false); + } + + private static void assertZippedReport(File result, Path workDir, String reportFileName) + throws IOException { + assertThat(result.getName(), is(equalTo(zipFileName(reportFileName)))); + assertThat(Files.exists(workDir.resolve(reportFileName)), is(false)); + } + + private static void assertNotZipped(File result, Path workDir, String reportFileName) { + assertThat(result.getName(), is(equalTo(reportFileName))); + assertThat(Files.exists(workDir.resolve(zipFileName(reportFileName))), is(false)); + assertThat(Files.exists(workDir.resolve(reportFileName)), is(true)); + } + + private static void assertZipEntries( + File zipFile, Matcher> matcher) throws IOException { + assertThat(listZipEntryNames(zipFile), matcher); + } + + private static String zipFileName(String reportFileName) { + int dotIndex = reportFileName.lastIndexOf('.'); + if (dotIndex > 0) { + return reportFileName.substring(0, dotIndex) + ".zip"; + } + return reportFileName + ".zip"; + } + + private static List listZipEntryNames(File zipFile) throws IOException { + try (ZipFile zip = new ZipFile(zipFile)) { + return zip.stream().map(ZipEntry::getName).sorted().collect(Collectors.toList()); + } + } + + private static void deleteDirectory(Path dir) throws IOException { + if (dir != null && Files.exists(dir)) { + FileUtils.deleteDirectory(dir.toFile()); + } + } + private static ReportData setupReportData() { ReportData reportData = ReportTestUtils.getTestReportData(); AlertNode root = new AlertNode(0, "Alerts"); diff --git a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportApiUnitTest.java b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportApiUnitTest.java index 7a491f758f4..9bedb08325c 100644 --- a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportApiUnitTest.java +++ b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportApiUnitTest.java @@ -34,6 +34,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; +import java.io.File; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; @@ -47,6 +48,7 @@ import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.model.Model; import org.zaproxy.zap.extension.api.ApiException; +import org.zaproxy.zap.extension.api.ApiResponseElement; import org.zaproxy.zap.model.Context; import org.zaproxy.zap.utils.I18N; @@ -74,6 +76,8 @@ void setUp() throws Exception { params.put(ReportApi.PARAM_TEMPLATE, "traditional-html-plus"); template = ReportTestUtils.getTemplateFromYamlFile("traditional-html-plus"); when(extReports.getTemplateByConfigName(anyString())).thenReturn(template); + when(extReports.generateReport(any(), any(), anyString(), anyBoolean())) + .thenAnswer(invocation -> new File((String) invocation.getArgument(2))); reportDataCaptor = ArgumentCaptor.forClass(ReportData.class); } @@ -292,6 +296,35 @@ void shouldPopulateReportDisplay() throws Exception { assertThat(displayCaptor.getValue(), is(display)); } + @Test + void shouldPopulateReportZip() throws Exception { + // Given + params.put(ReportApi.PARAM_ZIP, true); + + // When + reportApi.handleApiAction(ReportApi.ACTION_GENERATE, params); + + // Then + verify(extReports) + .generateReport(reportDataCaptor.capture(), any(), anyString(), anyBoolean()); + assertThat(reportDataCaptor.getValue().isZipReport(), is(true)); + } + + @Test + void shouldDisableDisplayWhenZipEnabled() throws Exception { + // Given + params.put(ReportApi.PARAM_ZIP, true); + params.put(ReportApi.PARAM_DISPLAY, true); + ArgumentCaptor displayCaptor = ArgumentCaptor.forClass(boolean.class); + + // When + reportApi.handleApiAction(ReportApi.ACTION_GENERATE, params); + + // Then + verify(extReports).generateReport(any(), any(), anyString(), displayCaptor.capture()); + assertThat(displayCaptor.getValue(), is(false)); + } + @Test void shouldPopulateOptionalParamsWithDefaultValues() throws Exception { // Given @@ -336,7 +369,8 @@ void shouldPopulateOptionalParamsWithDefaultValues() throws Exception { () -> assertThat(reportData.isIncludeRisk(Alert.RISK_MEDIUM), is(true)), () -> assertThat(reportData.isIncludeRisk(Alert.RISK_HIGH), is(true)), () -> assertThat(reportFilePathCaptor.getValue(), is(expectedReportFilePath)), - () -> assertThat(displayCaptor.getValue(), is(false))); + () -> assertThat(displayCaptor.getValue(), is(false)), + () -> assertThat(reportData.isZipReport(), is(false))); } @Test @@ -484,4 +518,29 @@ void fileNameShouldOverrideFileNamePattern() throws Exception { () -> assertThat(reportFilePathCaptor.getValue(), is(fileNamePath)), () -> assertThat(reportFilePathCaptor.getValue(), is(not(fileNamePatternPath)))); } + + @Test + void shouldReturnGeneratedReportPath() throws Exception { + // Given + String reportDirectory = System.getProperty("java.io.tmpdir"); + String reportFileName = "testrpt"; + String htmlReportPath = + Paths.get(reportDirectory, reportFileName + '.' + template.getExtension()) + .toString(); + File zipReport = new File(reportDirectory, reportFileName + ".zip"); + params.put(ReportApi.PARAM_REPORT_DIRECTORY, reportDirectory); + params.put(ReportApi.PARAM_REPORT_FILE_NAME, reportFileName); + params.put(ReportApi.PARAM_ZIP, true); + when(extReports.generateReport(any(), any(), anyString(), anyBoolean())) + .thenReturn(zipReport); + + // When + ApiResponseElement response = + (ApiResponseElement) reportApi.handleApiAction(ReportApi.ACTION_GENERATE, params); + + // Then + assertAll( + () -> assertThat(response.getValue(), is(zipReport.getAbsolutePath())), + () -> assertThat(response.getValue(), is(not(htmlReportPath)))); + } } diff --git a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportOutputOptionsUnitTest.java b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportOutputOptionsUnitTest.java new file mode 100644 index 00000000000..1bd21c70abf --- /dev/null +++ b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportOutputOptionsUnitTest.java @@ -0,0 +1,55 @@ +/* + * Zed Attack Proxy (ZAP) and its related class files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2026 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.zaproxy.addon.reports; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import org.junit.jupiter.api.Test; + +class ReportOutputOptionsUnitTest { + + @Test + void shouldDisableDisplayWhenZipSelected() { + assertThat(ReportOutputOptions.resolveDisplay(true, true), is(false)); + assertThat(ReportOutputOptions.resolveDisplay(true, false), is(false)); + } + + @Test + void shouldKeepDisplayWhenZipNotSelected() { + assertThat(ReportOutputOptions.resolveDisplay(false, true), is(true)); + assertThat(ReportOutputOptions.resolveDisplay(false, false), is(false)); + } + + @Test + void shouldPreferZipWhenBothEnabled() { + assertThat(ReportOutputOptions.bothEnabled(true, true), is(true)); + assertThat(ReportOutputOptions.bothEnabled(true, false), is(false)); + assertThat(ReportOutputOptions.bothEnabled(false, true), is(false)); + } + + @Test + void shouldPreferZipWhenBothInitiallySelected() { + boolean[] resolved = ReportOutputOptions.resolveInitialSelection(true, true); + + assertThat(resolved[0], is(true)); + assertThat(resolved[1], is(false)); + } +} diff --git a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportParamUnitTest.java b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportParamUnitTest.java index 29c17b93371..628235a51c3 100644 --- a/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportParamUnitTest.java +++ b/addOns/reports/src/test/java/org/zaproxy/addon/reports/ReportParamUnitTest.java @@ -73,6 +73,7 @@ void shouldLoadDefaultParams() { reportParam.getTemplateDirectory(), is(equalTo(Constant.getZapHome() + "/reports/"))); assertThat(reportParam.isDisplayReport(), is(equalTo(true))); + assertThat(reportParam.isZipReport(), is(equalTo(false))); } @Test @@ -87,6 +88,7 @@ void shouldLoadExpectedParams() throws IOException { config.addProperty("reports.reportDir", "/test/123/"); config.addProperty("reports.templateDir", tempDir.getAbsolutePath()); config.addProperty("reports.display", "false"); + config.addProperty("reports.zip", "true"); // When reportParam.load(config); @@ -99,6 +101,7 @@ void shouldLoadExpectedParams() throws IOException { assertThat(reportParam.getReportDirectory(), is(equalTo("/test/123/"))); assertThat(reportParam.getTemplateDirectory(), is(equalTo(tempDir.getAbsolutePath()))); assertThat(reportParam.isDisplayReport(), is(equalTo(false))); + assertThat(reportParam.isZipReport(), is(equalTo(true))); } @Test @@ -115,6 +118,7 @@ void shouldSetSpecifiedParams() { reportParam.setReportDirectory("/test/123/"); reportParam.setTemplateDirectory("/test/123/"); reportParam.setDisplayReport(false); + reportParam.setZipReport(true); // Then assertThat(config.getString("reports.title"), is(equalTo("Report title"))); @@ -124,5 +128,6 @@ void shouldSetSpecifiedParams() { assertThat(config.getString("reports.reportDir"), is(equalTo("/test/123/"))); assertThat(config.getString("reports.templateDir"), is(equalTo("/test/123/"))); assertThat(config.getBoolean("reports.display"), is(equalTo(false))); + assertThat(config.getBoolean("reports.zip"), is(equalTo(true))); } } diff --git a/addOns/reports/src/test/java/org/zaproxy/addon/reports/automation/ReportJobUnitTest.java b/addOns/reports/src/test/java/org/zaproxy/addon/reports/automation/ReportJobUnitTest.java index fc2dfe7ad5a..7b2729042b0 100644 --- a/addOns/reports/src/test/java/org/zaproxy/addon/reports/automation/ReportJobUnitTest.java +++ b/addOns/reports/src/test/java/org/zaproxy/addon/reports/automation/ReportJobUnitTest.java @@ -146,7 +146,7 @@ void shouldReturnCustomConfigParams() { Map params = job.getCustomConfigParameters(); // Then - assertThat(params.size(), is(equalTo(7))); + assertThat(params.size(), is(equalTo(8))); assertThat( params, allOf( @@ -156,7 +156,8 @@ void shouldReturnCustomConfigParams() { hasKey("reportTitle"), hasKey("reportDescription"), hasKey("theme"), - hasKey("displayReport"))); + hasKey("displayReport"), + hasKey("zipReport"))); } @Test @@ -169,29 +170,29 @@ void shouldApplyCustomConfigParams() { String reportDescription = "reportDescription"; String theme = "theme"; Boolean displayReport = Boolean.TRUE; + Boolean zipReport = Boolean.TRUE; ReportJob job = createReportJob( - "parameters:\n" - + " template: " - + template - + "\n" - + " reportFile: " - + reportFile - + "\n" - + " reportDir: " - + reportDir - + "\n" - + " reportTitle: " - + reportTitle - + "\n" - + " reportDescription: " - + reportDescription - + "\n" - + " theme: " - + theme - + "\n" - + " displayReport: " - + displayReport); + """ + parameters: + template: %s + reportFile: %s + reportDir: %s + reportTitle: %s + reportDescription: %s + theme: %s + displayReport: %s + zipReport: %s + """ + .formatted( + template, + reportFile, + reportDir, + reportTitle, + reportDescription, + theme, + displayReport, + zipReport)); AutomationProgress progress = new AutomationProgress(); // When @@ -205,6 +206,103 @@ void shouldApplyCustomConfigParams() { assertThat(job.getParameters().getReportDescription(), is(equalTo(reportDescription))); assertThat(job.getParameters().getTheme(), is(equalTo(theme))); assertThat(job.getParameters().getDisplayReport(), is(equalTo(displayReport))); + assertThat(job.getParameters().getZipReport(), is(equalTo(zipReport))); + } + + @Test + void shouldPassZipReportToReportDataWhenRunning() throws IOException { + // Given + ReportJob job = + createReportJob( + """ + parameters: + template: template + reportFile: report-file + reportDir: report-dir + zipReport: true + """); + AutomationPlan plan = new AutomationPlan(); + AutomationProgress progress = plan.getProgress(); + AutomationEnvironment env = plan.getEnv(); + ContextWrapper contextWrapper = mock(ContextWrapper.class); + given(contextWrapper.getUrls()).willReturn(Collections.singletonList("")); + env.setContexts(Arrays.asList(contextWrapper)); + + Template template = mock(Template.class); + given(template.getExtension()).willReturn("ext"); + given(extensionReports.getTemplateByConfigName(anyString())).willReturn(template); + + job.verifyParameters(progress); + + ArgumentCaptor reportDataCapture = ArgumentCaptor.forClass(ReportData.class); + given( + extensionReports.generateReport( + reportDataCapture.capture(), any(), anyString(), anyBoolean())) + .willReturn(mock(File.class)); + + job.setPlan(plan); + + // When + job.runJob(env, progress); + + // Then + assertThat(progress.hasWarnings(), is(equalTo(false))); + assertThat(progress.hasErrors(), is(equalTo(false))); + assertThat(reportDataCapture.getValue().isZipReport(), is(equalTo(true))); + } + + @Test + void shouldWarnAndIgnoreDisplayWhenBothEnabled() throws IOException { + // Given + ReportJob job = + createReportJob( + """ + parameters: + template: template + reportFile: report-file + reportDir: report-dir + zipReport: true + displayReport: true + """); + AutomationPlan plan = new AutomationPlan(); + AutomationProgress progress = plan.getProgress(); + AutomationEnvironment env = plan.getEnv(); + ContextWrapper contextWrapper = mock(ContextWrapper.class); + given(contextWrapper.getUrls()).willReturn(Collections.singletonList("")); + env.setContexts(Arrays.asList(contextWrapper)); + + Template template = mock(Template.class); + given(template.getExtension()).willReturn("ext"); + given(extensionReports.getTemplateByConfigName(anyString())).willReturn(template); + + job.verifyParameters(progress); + + ArgumentCaptor reportDataCapture = ArgumentCaptor.forClass(ReportData.class); + ArgumentCaptor displayCaptor = ArgumentCaptor.forClass(boolean.class); + given( + extensionReports.generateReport( + reportDataCapture.capture(), + any(), + anyString(), + displayCaptor.capture())) + .willReturn(mock(File.class)); + + job.setPlan(plan); + + // When + job.runJob(env, progress); + + // Then + assertThat(reportDataCapture.getValue().isZipReport(), is(equalTo(true))); + assertThat(displayCaptor.getValue(), is(equalTo(false))); + assertThat(progress.hasWarnings(), is(equalTo(true))); + assertThat( + progress.getWarnings().get(0), + is( + equalTo( + Constant.messages.getString( + "reports.automation.warn.zipanddisplay", job.getName())))); + assertThat(progress.hasErrors(), is(equalTo(false))); } @Test