EvidenceReportWriter.java
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Egothor
// Copyright 2026 Accenture
package org.egothor.methodatlas.evidencereport;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Serialises an {@link EvidenceReport} to disk as a Markdown document.
*
* <p>
* Package-private because nothing outside the {@code evidencereport} package needs
* direct access; {@link EvidenceReportFacade} is the sole external caller.
* </p>
*/
final class EvidenceReportWriter {
private EvidenceReportWriter() {
// Utility class.
}
/**
* Writes {@code report} to {@code outputFile} as UTF-8 Markdown.
*
* @param report the evidence report to serialise
* @param outputFile destination path; parent directory must exist
* @throws IOException if the file cannot be written
*/
/* default */ static void write(EvidenceReport report, Path outputFile) throws IOException {
try (PrintWriter out = new PrintWriter(
Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8))) {
writeHeader(out, report);
writeSummary(out, report);
writeMethods(out, report);
}
}
private static void writeHeader(PrintWriter out, EvidenceReport report) {
out.println("# MethodAtlas Security Evidence Report");
out.println();
out.println("**Generated:** " + report.generatedUtc() + " ");
out.println("**Tool version:** " + report.toolVersion() + " ");
if (!report.scanRoots().isEmpty()) {
out.print("**Scan roots:** ");
StringBuilder sb = new StringBuilder();
for (String root : report.scanRoots()) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append('`').append(root).append('`');
}
out.println(sb + " ");
}
out.println("**Total methods scanned:** " + report.totalScanned() + " ");
out.println("**Security-relevant methods:** " + report.securityRelevantCount() + " ");
out.println();
}
private static void writeSummary(PrintWriter out, EvidenceReport report) {
out.println("## Summary");
out.println();
Map<String, Integer> domainCounts = new LinkedHashMap<>();
for (List<EvidenceReportEntry> entries : report.byClass().values()) {
for (EvidenceReportEntry entry : entries) {
if (entry.aiTags() != null) {
for (String tag : entry.aiTags()) {
domainCounts.merge(tag, 1, Integer::sum);
}
}
}
}
if (domainCounts.isEmpty()) {
out.println("No security-relevant methods were found in this scan.");
} else {
out.println("| Domain | Count |");
out.println("|--------|-------|");
for (Map.Entry<String, Integer> e : domainCounts.entrySet()) {
out.println("| " + e.getKey() + " | " + e.getValue() + " |");
}
}
out.println();
}
private static void writeMethods(PrintWriter out, EvidenceReport report) {
if (report.byClass().isEmpty()) {
return;
}
out.println("## Security Test Methods");
out.println();
for (Map.Entry<String, List<EvidenceReportEntry>> classEntry : report.byClass().entrySet()) {
out.println("### " + classEntry.getKey());
out.println();
for (EvidenceReportEntry entry : classEntry.getValue()) {
writeEntry(out, entry);
}
}
}
@SuppressWarnings("PMD.NPathComplexity")
private static void writeEntry(PrintWriter out, EvidenceReportEntry entry) {
out.println("#### " + entry.method());
out.println();
out.println("| Field | Value |");
out.println("|-------|-------|");
String dispName = entry.aiDisplayName() != null ? entry.aiDisplayName()
: entry.displayName() != null ? entry.displayName() : "";
if (!dispName.isEmpty()) {
out.println("| Display name | " + mdEscape(dispName) + " |");
}
if (!entry.sourceTags().isEmpty()) {
out.println("| Source tags | " + String.join(", ", entry.sourceTags()) + " |");
}
if (entry.aiTags() != null && !entry.aiTags().isEmpty()) {
out.println("| AI tags | " + String.join(", ", entry.aiTags()) + " |");
}
if (entry.aiSecurityRelevant() != null) {
out.println("| AI security relevant | " + entry.aiSecurityRelevant() + " |");
}
if (entry.aiConfidence() != null) {
out.println("| Confidence | " + entry.aiConfidence() + " |");
}
if (entry.aiInteractionScore() != null) {
out.println("| Interaction score | " + entry.aiInteractionScore() + " |");
}
if (entry.contentHash() != null) {
out.println("| Content hash | `" + entry.contentHash() + "` |");
}
if (entry.aiReason() != null && !entry.aiReason().isBlank()) {
out.println("| Reason | " + mdEscape(entry.aiReason()) + " |");
}
if (!entry.aiCwe().isEmpty()) {
out.println("| CWE | " + String.join(", ", entry.aiCwe()) + " |");
}
if (entry.beginLine() > 0) {
out.println("| Line | " + entry.beginLine() + " |");
}
out.println();
}
/** Escapes pipe characters so they do not break Markdown table cells. */
private static String mdEscape(String value) {
return value.replace("|", "\\|");
}
}