Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions addOns/reports/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -419,65 +425,69 @@ 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());
if (display) {
if ("HTML".equals(template.getFormat())) {
DesktopUtils.openUrlInBrowser(file.toURI());
} else {
Desktop desktop = Desktop.getDesktop();
desktop.open(file);
Desktop.getDesktop().open(file);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The options should be mutually exclusive (or simply note the display does not work for the ZIP).

}
}
return file;
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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}));
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +44,8 @@ public class ReportData {
private List<String> sections = new ArrayList<>();
private String theme;

@Getter @Setter private boolean zipReport;

@Deprecated
public ReportData() {}

Expand Down
Loading
Loading