| 1 | // SPDX-License-Identifier: Apache-2.0 | |
| 2 | // Copyright 2026 Egothor | |
| 3 | // Copyright 2026 Accenture | |
| 4 | package org.egothor.methodatlas.evidencereport; | |
| 5 | ||
| 6 | import java.io.IOException; | |
| 7 | import java.io.PrintWriter; | |
| 8 | import java.nio.charset.StandardCharsets; | |
| 9 | import java.nio.file.Files; | |
| 10 | import java.nio.file.Path; | |
| 11 | import java.util.LinkedHashMap; | |
| 12 | import java.util.List; | |
| 13 | import java.util.Map; | |
| 14 | ||
| 15 | /** | |
| 16 | * Serialises an {@link EvidenceReport} to disk as a Markdown document. | |
| 17 | * | |
| 18 | * <p> | |
| 19 | * Package-private because nothing outside the {@code evidencereport} package needs | |
| 20 | * direct access; {@link EvidenceReportFacade} is the sole external caller. | |
| 21 | * </p> | |
| 22 | */ | |
| 23 | final class EvidenceReportWriter { | |
| 24 | ||
| 25 | private EvidenceReportWriter() { | |
| 26 | // Utility class. | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * Writes {@code report} to {@code outputFile} as UTF-8 Markdown. | |
| 31 | * | |
| 32 | * @param report the evidence report to serialise | |
| 33 | * @param outputFile destination path; parent directory must exist | |
| 34 | * @throws IOException if the file cannot be written | |
| 35 | */ | |
| 36 | /* default */ static void write(EvidenceReport report, Path outputFile) throws IOException { | |
| 37 | try (PrintWriter out = new PrintWriter( | |
| 38 | Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8))) { | |
| 39 | writeHeader(out, report); | |
| 40 | writeSummary(out, report); | |
| 41 | writeMethods(out, report); | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | private static void writeHeader(PrintWriter out, EvidenceReport report) { | |
| 46 | out.println("# MethodAtlas Security Evidence Report"); | |
| 47 | out.println(); | |
| 48 | out.println("**Generated:** " + report.generatedUtc() + " "); | |
| 49 | out.println("**Tool version:** " + report.toolVersion() + " "); | |
| 50 |
2
1. writeHeader : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeHeader : removed conditional - replaced equality check with true → NO_COVERAGE |
if (!report.scanRoots().isEmpty()) { |
| 51 | out.print("**Scan roots:** "); | |
| 52 | StringBuilder sb = new StringBuilder(); | |
| 53 | for (String root : report.scanRoots()) { | |
| 54 |
3
1. writeHeader : removed conditional - replaced comparison check with true → NO_COVERAGE 2. writeHeader : changed conditional boundary → NO_COVERAGE 3. writeHeader : removed conditional - replaced comparison check with false → NO_COVERAGE |
if (sb.length() > 0) { |
| 55 | sb.append(", "); | |
| 56 | } | |
| 57 | sb.append('`').append(root).append('`'); | |
| 58 | } | |
| 59 | out.println(sb + " "); | |
| 60 | } | |
| 61 | out.println("**Total methods scanned:** " + report.totalScanned() + " "); | |
| 62 | out.println("**Security-relevant methods:** " + report.securityRelevantCount() + " "); | |
| 63 | out.println(); | |
| 64 | } | |
| 65 | ||
| 66 | private static void writeSummary(PrintWriter out, EvidenceReport report) { | |
| 67 | out.println("## Summary"); | |
| 68 | out.println(); | |
| 69 | Map<String, Integer> domainCounts = new LinkedHashMap<>(); | |
| 70 | for (List<EvidenceReportEntry> entries : report.byClass().values()) { | |
| 71 | for (EvidenceReportEntry entry : entries) { | |
| 72 |
2
1. writeSummary : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeSummary : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiTags() != null) { |
| 73 | for (String tag : entry.aiTags()) { | |
| 74 | domainCounts.merge(tag, 1, Integer::sum); | |
| 75 | } | |
| 76 | } | |
| 77 | } | |
| 78 | } | |
| 79 |
2
1. writeSummary : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeSummary : removed conditional - replaced equality check with true → NO_COVERAGE |
if (domainCounts.isEmpty()) { |
| 80 | out.println("No security-relevant methods were found in this scan."); | |
| 81 | } else { | |
| 82 | out.println("| Domain | Count |"); | |
| 83 | out.println("|--------|-------|"); | |
| 84 | for (Map.Entry<String, Integer> e : domainCounts.entrySet()) { | |
| 85 | out.println("| " + e.getKey() + " | " + e.getValue() + " |"); | |
| 86 | } | |
| 87 | } | |
| 88 | out.println(); | |
| 89 | } | |
| 90 | ||
| 91 | private static void writeMethods(PrintWriter out, EvidenceReport report) { | |
| 92 |
2
1. writeMethods : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeMethods : removed conditional - replaced equality check with true → NO_COVERAGE |
if (report.byClass().isEmpty()) { |
| 93 | return; | |
| 94 | } | |
| 95 | out.println("## Security Test Methods"); | |
| 96 | out.println(); | |
| 97 | for (Map.Entry<String, List<EvidenceReportEntry>> classEntry : report.byClass().entrySet()) { | |
| 98 | out.println("### " + classEntry.getKey()); | |
| 99 | out.println(); | |
| 100 | for (EvidenceReportEntry entry : classEntry.getValue()) { | |
| 101 | writeEntry(out, entry); | |
| 102 | } | |
| 103 | } | |
| 104 | } | |
| 105 | ||
| 106 | @SuppressWarnings("PMD.NPathComplexity") | |
| 107 | private static void writeEntry(PrintWriter out, EvidenceReportEntry entry) { | |
| 108 | out.println("#### " + entry.method()); | |
| 109 | out.println(); | |
| 110 | out.println("| Field | Value |"); | |
| 111 | out.println("|-------|-------|"); | |
| 112 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
String dispName = entry.aiDisplayName() != null ? entry.aiDisplayName() |
| 113 |
2
1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE |
: entry.displayName() != null ? entry.displayName() : ""; |
| 114 |
2
1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE |
if (!dispName.isEmpty()) { |
| 115 | out.println("| Display name | " + mdEscape(dispName) + " |"); | |
| 116 | } | |
| 117 |
2
1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE |
if (!entry.sourceTags().isEmpty()) { |
| 118 | out.println("| Source tags | " + String.join(", ", entry.sourceTags()) + " |"); | |
| 119 | } | |
| 120 |
4
1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 3. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 4. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiTags() != null && !entry.aiTags().isEmpty()) { |
| 121 | out.println("| AI tags | " + String.join(", ", entry.aiTags()) + " |"); | |
| 122 | } | |
| 123 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiSecurityRelevant() != null) { |
| 124 | out.println("| AI security relevant | " + entry.aiSecurityRelevant() + " |"); | |
| 125 | } | |
| 126 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiConfidence() != null) { |
| 127 | out.println("| Confidence | " + entry.aiConfidence() + " |"); | |
| 128 | } | |
| 129 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiInteractionScore() != null) { |
| 130 | out.println("| Interaction score | " + entry.aiInteractionScore() + " |"); | |
| 131 | } | |
| 132 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.contentHash() != null) { |
| 133 | out.println("| Content hash | `" + entry.contentHash() + "` |"); | |
| 134 | } | |
| 135 |
4
1. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 3. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 4. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (entry.aiReason() != null && !entry.aiReason().isBlank()) { |
| 136 | out.println("| Reason | " + mdEscape(entry.aiReason()) + " |"); | |
| 137 | } | |
| 138 |
2
1. writeEntry : removed conditional - replaced equality check with true → NO_COVERAGE 2. writeEntry : removed conditional - replaced equality check with false → NO_COVERAGE |
if (!entry.aiCwe().isEmpty()) { |
| 139 | out.println("| CWE | " + String.join(", ", entry.aiCwe()) + " |"); | |
| 140 | } | |
| 141 |
3
1. writeEntry : removed conditional - replaced comparison check with false → NO_COVERAGE 2. writeEntry : changed conditional boundary → NO_COVERAGE 3. writeEntry : removed conditional - replaced comparison check with true → NO_COVERAGE |
if (entry.beginLine() > 0) { |
| 142 | out.println("| Line | " + entry.beginLine() + " |"); | |
| 143 | } | |
| 144 | out.println(); | |
| 145 | } | |
| 146 | ||
| 147 | /** Escapes pipe characters so they do not break Markdown table cells. */ | |
| 148 | private static String mdEscape(String value) { | |
| 149 |
1
1. mdEscape : replaced return value with "" for org/egothor/methodatlas/evidencereport/EvidenceReportWriter::mdEscape → NO_COVERAGE |
return value.replace("|", "\\|"); |
| 150 | } | |
| 151 | } | |
Mutations | ||
| 50 |
1.1 2.2 |
|
| 54 |
1.1 2.2 3.3 |
|
| 72 |
1.1 2.2 |
|
| 79 |
1.1 2.2 |
|
| 92 |
1.1 2.2 |
|
| 112 |
1.1 2.2 |
|
| 113 |
1.1 2.2 |
|
| 114 |
1.1 2.2 |
|
| 117 |
1.1 2.2 |
|
| 120 |
1.1 2.2 3.3 4.4 |
|
| 123 |
1.1 2.2 |
|
| 126 |
1.1 2.2 |
|
| 129 |
1.1 2.2 |
|
| 132 |
1.1 2.2 |
|
| 135 |
1.1 2.2 3.3 4.4 |
|
| 138 |
1.1 2.2 |
|
| 141 |
1.1 2.2 3.3 |
|
| 149 |
1.1 |