From ca834fe7ab1cb31b95d1e9eca6e2c9ca1b045fcc Mon Sep 17 00:00:00 2001 From: Oleg Kopysov Date: Wed, 14 Aug 2024 09:33:01 +0300 Subject: [PATCH 1/2] feat: Initial version of HTML report generation for single scan Signed-off-by: Oleg Kopysov --- pom.xml | 9 + .../com/lpvs/LicensePreValidationService.java | 61 ++- .../lpvs/entity/report/LPVSReportBuilder.java | 483 ++++++++++++++++++ .../lpvs/service/scan/LPVSDetectService.java | 18 +- .../java/com/lpvs/util/LPVSCommentUtil.java | 71 +-- .../templates/report_single_scan.html | 141 +++++ .../lpvs/LicensePreValidationServiceTest.java | 23 + .../entity/report/LPVSReportBuilderTest.java | 189 +++++++ .../service/scan/LPVSDetectServiceTest.java | 109 +++- .../com/lpvs/util/LPVSCommentUtilTest.java | 29 -- 10 files changed, 985 insertions(+), 148 deletions(-) create mode 100644 src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java create mode 100644 src/main/resources/templates/report_single_scan.html create mode 100644 src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java diff --git a/pom.xml b/pom.xml index 3aa710e5..f944522a 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,11 @@ + + org.apache.maven + maven-model + 3.8.6 + org.springframework.boot spring-boot-starter-web @@ -129,6 +134,10 @@ jakarta.servlet-api 6.1.0 + + org.springframework.boot + spring-boot-starter-thymeleaf + diff --git a/src/main/java/com/lpvs/LicensePreValidationService.java b/src/main/java/com/lpvs/LicensePreValidationService.java index 8cb02330..6bb6f2ba 100644 --- a/src/main/java/com/lpvs/LicensePreValidationService.java +++ b/src/main/java/com/lpvs/LicensePreValidationService.java @@ -7,6 +7,8 @@ package com.lpvs; import lombok.extern.slf4j.Slf4j; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -19,6 +21,8 @@ import com.lpvs.util.LPVSExitHandler; +import java.io.FileReader; + /** * The main class for the License Pre-Validation Service (LPVS) application. * This class configures and launches the LPVS Spring Boot application. @@ -74,6 +78,21 @@ public static void main(String[] args) { } } + /** + * Retrieves the version of the LicensePreValidationService using the provided MavenXpp3Reader object. + * + * @param reader the MavenXpp3Reader object used to read the pom.xml file + * @return the version of the LicensePreValidationService, or "latest" if an error occurs + */ + public static String getVersion(MavenXpp3Reader reader) { + try (FileReader fileReader = new FileReader("pom.xml")) { + Model model = reader.read(fileReader); + return model.getVersion(); + } catch (Exception e) { + return "latest"; + } + } + /** * Configures and retrieves an asynchronous task executor bean. * @@ -93,32 +112,20 @@ public TaskExecutor getAsyncExecutor() { * @return the emblem as a String */ protected static String getEmblem() { - StringBuilder emblem = new StringBuilder(); - emblem.append("\n"); - emblem.append( - " .----------------. .----------------. .----------------. .----------------. \n"); - emblem.append( - " | .--------------. | | .--------------. | | .--------------. | | .--------------. |\n"); - emblem.append( - " | | _____ | | | | ______ | | | | ____ ____ | | | | _______ | |\n"); - emblem.append( - " | | |_ _| | | | | |_ __ \\ | | | ||_ _| |_ _| | | | | / ___ | | |\n"); - emblem.append( - " | | | | | | | | | |__) | | | | | \\ \\ / / | | | | | (__ \\_| | |\n"); - emblem.append( - " | | | | _ | | | | | ___/ | | | | \\ \\ / / | | | | '.___`-. | |\n"); - emblem.append( - " | | _| |__/ | | | | | _| |_ | | | | \\ ' / | | | | |`\\____) | | |\n"); - emblem.append( - " | | |________| | | | | |_____| | | | | \\_/ | | | | |_______.' | |\n"); - emblem.append( - " | | | | | | | | | | | | | | | |\n"); - emblem.append( - " | '--------------' | | '--------------' | | '--------------' | | '--------------' |\n"); - emblem.append( - " '----------------' '----------------' '----------------' '----------------' \n"); - emblem.append( - " :: License Pre-Validation Service :: (v1.5.2)\n"); - return emblem.toString(); + return "\n" + + " .----------------. .----------------. .----------------. .----------------. \n" + + " | .--------------. | | .--------------. | | .--------------. | | .--------------. |\n" + + " | | _____ | | | | ______ | | | | ____ ____ | | | | _______ | |\n" + + " | | |_ _| | | | | |_ __ \\ | | | ||_ _| |_ _| | | | | / ___ | | |\n" + + " | | | | | | | | | |__) | | | | | \\ \\ / / | | | | | (__ \\_| | |\n" + + " | | | | _ | | | | | ___/ | | | | \\ \\ / / | | | | '.___`-. | |\n" + + " | | _| |__/ | | | | | _| |_ | | | | \\ ' / | | | | |`\\____) | | |\n" + + " | | |________| | | | | |_____| | | | | \\_/ | | | | |_______.' | |\n" + + " | | | | | | | | | | | | | | | |\n" + + " | '--------------' | | '--------------' | | '--------------' | | '--------------' |\n" + + " '----------------' '----------------' '----------------' '----------------' \n" + + " :: License Pre-Validation Service :: (" + + getVersion(new MavenXpp3Reader()) + + ")\n"; } } diff --git a/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java new file mode 100644 index 00000000..d4a42be8 --- /dev/null +++ b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java @@ -0,0 +1,483 @@ +/** + * Copyright (c) 2024, Samsung Electronics Co., Ltd. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +package com.lpvs.entity.report; + +import com.lpvs.LicensePreValidationService; +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.service.LPVSLicenseService; +import io.micrometer.common.util.StringUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Collectors; + +/** + * A class responsible for building reports based on the results of license scanning. + */ +@Component +public class LPVSReportBuilder { + + /** + * Creates a new instance of the LPVSReportBuilder class. + * + * @param templateEngine the template engine to use for generating reports + */ + @Autowired + public LPVSReportBuilder(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + /** + * The template engine to use for generating reports. + */ + private final TemplateEngine templateEngine; + + /** + * The type of license detection scanner. + */ + @Value("${scanner:scanoss}") + private String scannerType; + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private Map>>>>>> + detectedLicenseInfo = null; + + private final String permitted = "PERMITTED"; + private final String restricted = "RESTRICTED"; + private final String prohibited = "PROHIBITED"; + private final String unreviewed = "UNREVIEWED"; + + /** + * Generates an HTML report for a single scan. + * + * @param path the path to the source folder for scan or pull request URL + * @param scanResults the results of the license scan + * @param conflicts a list of license conflicts found during the scan + * @return the HTML code of the generated report + */ + public String generateHtmlReportSingleScan( + String path, + List scanResults, + List> conflicts) { + Context context = new Context(); + String date = sdf.format(new Date()); + + context.setVariable("title", "Report-LPVS-" + date); + context.setVariable("scanDate", date); + context.setVariable("codeLocation", path); + context.setVariable("usedScanner", scannerType); + context.setVariable( + "lpvsVersion", LicensePreValidationService.getVersion(new MavenXpp3Reader())); + + groupScanResultsForLicenseTable(scanResults); + long prohibitedLicenses = getDetectedLicenseCountByType(prohibited); + long restrictedLicenses = getDetectedLicenseCountByType(restricted); + long unreviewedLicenses = getDetectedLicenseCountByType(unreviewed); + long licenseDetected = prohibitedLicenses + restrictedLicenses + unreviewedLicenses; + + context.setVariable("licenseDetected", licenseDetected); + context.setVariable("prohibitedLicenses", prohibitedLicenses); + context.setVariable("restrictedLicenses", restrictedLicenses); + context.setVariable("unreviewedLicenses", unreviewedLicenses); + + if (scanResults != null && !scanResults.isEmpty()) { + context.setVariable("licenseTable", generateLicenseTable()); + } else { + context.setVariable("licenseTable", null); + } + + if (conflicts != null && !conflicts.isEmpty()) { + context.setVariable("licenseConflicts", conflicts.size()); + context.setVariable("conflictTable", generateLicenseConflictsTable(conflicts)); + } else { + context.setVariable("licenseConflicts", 0); + context.setVariable("conflictTable", null); + } + + return templateEngine.process("report_single_scan", context); + } + + /** + * A class representing a group of elements with a count. + * + * @param the type of elements in the group + */ + @AllArgsConstructor + private static class GroupInfo { + /** + * The number of elements in the group. + */ + @Getter private long count; + + /** + * The elements in the group. + */ + private T elements; + } + + /** + * Function that returns the number of licenses detected for the given type. + * + * @param type the license type to count + * @return the number of licenses detected for the given type + */ + private long getDetectedLicenseCountByType(String type) { + if (detectedLicenseInfo == null || detectedLicenseInfo.get(type) == null) { + return 0; + } + return detectedLicenseInfo.get(type).elements.size(); + } + + /** + * Groups the scan results by license type for display in the license table. + * + * @param scanResults the results of the license scan + */ + private void groupScanResultsForLicenseTable(List scanResults) { + if (detectedLicenseInfo == null && scanResults != null && !scanResults.isEmpty()) { + List filesScanResults = getLpvsFilesFromScanResults(scanResults); + + detectedLicenseInfo = + filesScanResults.stream() + .collect( + Collectors.groupingBy( + LPVSFile -> + LPVSFile.getLicenses().stream() + .findFirst() + .get() + .getAccess(), + Collectors.collectingAndThen( + Collectors.groupingBy( + LPVSFile -> + LPVSFile.getLicenses().stream() + .findFirst() + .get() + .getSpdxId(), + Collectors.collectingAndThen( + Collectors.groupingBy( + LPVSFile -> + LPVSFile + .getComponentVendor() + + " / " + + LPVSFile + .getComponentName() + + ":::" + + LPVSFile + .getComponentUrl(), + Collectors + .collectingAndThen( + Collectors + .toList(), + elements -> + new GroupInfo<>( + elements + .size(), + elements))), + groupedByComponent -> + new GroupInfo<>( + groupedByComponent + .values() + .stream() + .mapToLong( + GroupInfo + ::getCount) + .sum(), + groupedByComponent))), + groupedByLicense -> + new GroupInfo<>( + groupedByLicense + .values() + .stream() + .mapToLong( + GroupInfo + ::getCount) + .sum(), + groupedByLicense)))); + } + } + + /** + * Function that returns the converted list of LPVS files with a single license. + * + * @param scanResults the results of the license scan + * @return a list of LPVS files with a single license + */ + private List getLpvsFilesFromScanResults(List scanResults) { + List filesScanResults = new ArrayList<>(); + for (LPVSFile file : scanResults) { + Set licenses = file.getLicenses(); + for (LPVSLicense license : licenses) { + LPVSFile file_ = + new LPVSFile() { + { + setFilePath(file.getFilePath()); + setAbsoluteFilePath(file.getAbsoluteFilePath()); + setSnippetType(file.getSnippetType()); + setSnippetMatch(file.getSnippetMatch()); + setMatchedLines(file.getMatchedLines()); + setLicenses(new HashSet<>(Collections.singletonList(license))); + setComponentFilePath(file.getComponentFilePath()); + setComponentFileUrl(file.getComponentFileUrl()); + setComponentName(file.getComponentName()); + setComponentLines(file.getComponentLines()); + setComponentUrl(file.getComponentUrl()); + setComponentVendor(file.getComponentVendor()); + setComponentVersion(file.getComponentVersion()); + } + }; + filesScanResults.add(file_); + } + } + return filesScanResults; + } + + /** + * Generates the license table HTML content. + * + * @return the HTML content for the license table + */ + private String generateLicenseTable() { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append("") + .append(""); + // Prohibited licenses + addBlockOfTableForLicenseType(htmlBuilder, prohibited); + // Restricted licenses + addBlockOfTableForLicenseType(htmlBuilder, restricted); + // Unreviewed licenses + addBlockOfTableForLicenseType(htmlBuilder, unreviewed); + // Permitted licenses + addBlockOfTableForLicenseType(htmlBuilder, permitted); + + htmlBuilder.append("
License Type / ExplanationLicense SPDX IDVendor / ComponentVersionRepository File PathComponent File PathMatched LinesMatch Value
"); + return htmlBuilder.toString(); + } + + /** + * Adds a block of HTML content for a specific license type to the license table. + * + * @param htmlBuilder the StringBuilder object to which the HTML content will be appended + * @param type the license type for which to add the block of HTML content + */ + private void addBlockOfTableForLicenseType(StringBuilder htmlBuilder, String type) { + long detectedLicenseCountByType = getDetectedLicenseCountByType(type); + boolean isNewRow; + if (detectedLicenseCountByType > 0) { + htmlBuilder.append(""); + isNewRow = true; + + htmlBuilder + .append(""); + switch (type.toUpperCase()) { + case prohibited: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + case restricted: + case unreviewed: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + case permitted: + htmlBuilder + .append("") + .append(type) + .append(""); + break; + default: + throw new IllegalStateException( + "Unexpected value for the license type: " + type); + } + htmlBuilder.append(" / "); + htmlBuilder.append(getExplanationForLicenseType(type)); + htmlBuilder.append(""); + + // license spdx + for (String licenseSpdxs : detectedLicenseInfo.get(type).elements.keySet()) { + if (!isNewRow) { + htmlBuilder.append(""); + isNewRow = true; + } + htmlBuilder + .append(""); + htmlBuilder.append(licenseSpdxs); + htmlBuilder.append(""); + + // vendor + component + for (String componentInfo : + detectedLicenseInfo + .get(type) + .elements + .get(licenseSpdxs) + .elements + .keySet()) { + if (!isNewRow) { + htmlBuilder.append(""); + isNewRow = true; + } + + htmlBuilder + .append(""); + htmlBuilder + .append("") + .append(componentInfo.split(":::")[0]) + .append(""); + htmlBuilder.append(""); + + // file info + for (LPVSFile fileInfo : + detectedLicenseInfo + .get(type) + .elements + .get(licenseSpdxs) + .elements + .get(componentInfo) + .elements) { + if (!isNewRow) { + htmlBuilder.append(""); + } + htmlBuilder + .append("") + .append(fileInfo.getComponentVersion()) + .append("") + .append(fileInfo.getFilePath()) + .append(""); + + if (!StringUtils.isBlank(fileInfo.getComponentFileUrl())) { + htmlBuilder + .append("") + .append(fileInfo.getComponentFilePath()) + .append(""); + } else { + htmlBuilder.append(fileInfo.getComponentFilePath()); + } + + htmlBuilder + .append("") + .append(fileInfo.getMatchedLines()) + .append("") + .append(fileInfo.getSnippetMatch()) + .append(""); + + htmlBuilder.append(""); + isNewRow = false; + } + } + } + } + } + + /** + * Generates the license conflicts table HTML content. + * + * @param conflicts a list of license conflicts + * @return the HTML content for the license conflicts table + */ + private String generateLicenseConflictsTable( + List> conflicts) { + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(""); + htmlBuilder + .append("") + .append("") + .append("") + .append(""); + + for (LPVSLicenseService.Conflict conflict : conflicts) { + htmlBuilder + .append("") + .append("") + .append("") + .append(""); + } + htmlBuilder.append("
ConflictExplanation
") + .append(conflict.l1) + .append(" and ") + .append(conflict.l2) + .append("") + .append(getExplanationForLicenseConflict(conflict.l1, conflict.l2)) + .append("
"); + return htmlBuilder.toString(); + } + + /** + * Retrieves the explanation for a specific license type. + * + * @param type the license type for which to retrieve the explanation + * @return the explanation for the specified license type + */ + private String getExplanationForLicenseType(String type) { + return switch (type.toUpperCase()) { + case prohibited -> + "This license prohibits the use of the licensed code in certain contexts, such as commercial software development."; + case restricted -> + "This license required compliance with specific obligations. It is crucial to carefully review and adhere to these obligations before using the licensed code."; + case unreviewed -> + "This license has not been reviewed thoroughly and may contain unknown risks or limitations. It is recommended to review these licenses carefully before using the licensed code."; + case permitted -> + "This license permits free usage, modification, and distribution of the licensed code without any restrictions."; + default -> + throw new IllegalStateException( + "Unexpected value for the license type: " + type); + }; + } + + /** + * Retrieves the explanation for a specific license conflict. + * + * @param lic1 the first license involved in the conflict + * @param lic2 the second license involved in the conflict + * @return the explanation for the specified license conflict + */ + private String getExplanationForLicenseConflict(String lic1, String lic2) { + return "These two licenses are incompatible due to their conflicting terms and conditions. It is recommended to resolve this conflict by choosing either " + + lic1 + + " or " + + lic2 + + " for the affected components."; + } +} diff --git a/src/main/java/com/lpvs/service/scan/LPVSDetectService.java b/src/main/java/com/lpvs/service/scan/LPVSDetectService.java index 8b3fb110..61f2195b 100644 --- a/src/main/java/com/lpvs/service/scan/LPVSDetectService.java +++ b/src/main/java/com/lpvs/service/scan/LPVSDetectService.java @@ -12,6 +12,7 @@ import java.util.Date; import java.util.List; +import com.lpvs.entity.report.LPVSReportBuilder; import com.lpvs.service.LPVSGitHubConnectionService; import com.lpvs.service.LPVSGitHubService; import com.lpvs.service.LPVSLicenseService; @@ -59,6 +60,11 @@ public class LPVSDetectService { */ private LPVSScanService scanService; + /** + * Component responsible for the generation of HTML reports. + */ + private LPVSReportBuilder reportBuilder; + /** * Trigger value to start a single scan of a pull request (optional). */ @@ -91,6 +97,7 @@ public class LPVSDetectService { * @param licenseService Service for license conflict analysis. * @param gitHubService Service for GitHub connection and operation. * @param scanServiceFactory Service for creating instance of the scanner. + * @param reportBuilder Service for generating HTML reports. */ @Autowired public LPVSDetectService( @@ -99,12 +106,14 @@ public LPVSDetectService( LPVSGitHubConnectionService gitHubConnectionService, LPVSLicenseService licenseService, LPVSGitHubService gitHubService, - LPVSScanServiceFactory scanServiceFactory) { + LPVSScanServiceFactory scanServiceFactory, + LPVSReportBuilder reportBuilder) { this.gitHubConnectionService = gitHubConnectionService; this.licenseService = licenseService; this.gitHubService = gitHubService; this.scanService = scanServiceFactory.createScanService(scannerType, isInternal); log.info("License detection scanner: " + scannerType); + this.reportBuilder = reportBuilder; } /** @@ -117,6 +126,7 @@ public void runSingleScan() { LPVSQueue webhookConfig = null; List scanResult = null; List> detectedConflicts = null; + String path = null; // Error case when both pull request scan and local files scan are set to true if (!StringUtils.isBlank(trigger) && !StringUtils.isBlank(localPath)) { @@ -138,6 +148,7 @@ public void runSingleScan() { detectedConflicts = licenseService.findConflicts(webhookConfig, scanResult); generateReport = true; + path = HtmlUtils.htmlEscape(trigger); log.info("Single scan of pull request completed."); } catch (Exception ex) { log.error("Single scan of pull request failed with error: " + ex.getMessage()); @@ -165,6 +176,7 @@ public void runSingleScan() { detectedConflicts = licenseService.findConflicts(webhookConfig, scanResult); generateReport = true; + path = localFile.getAbsolutePath(); log.info("Single scan of local file(s) completed."); } else { throw new Exception("File path does not exist: " + localPath); @@ -187,8 +199,8 @@ public void runSingleScan() { File folder = new File(folderPath); if (folder.exists() && folder.isDirectory()) { String reportFile = - LPVSCommentUtil.buildHTMLComment( - webhookConfig, scanResult, detectedConflicts); + reportBuilder.generateHtmlReportSingleScan( + path, scanResult, detectedConflicts); LPVSCommentUtil.saveHTMLToFile(reportFile, report.getAbsolutePath()); } else { log.error("Error: The parent directory '" + folder.getPath() + "' does not exist."); diff --git a/src/main/java/com/lpvs/util/LPVSCommentUtil.java b/src/main/java/com/lpvs/util/LPVSCommentUtil.java index bacb9136..dc042447 100644 --- a/src/main/java/com/lpvs/util/LPVSCommentUtil.java +++ b/src/main/java/com/lpvs/util/LPVSCommentUtil.java @@ -139,75 +139,6 @@ public static String reportCommentBuilder( return commitCommentBuilder.toString(); } - /** - * Generates a formatted string for an HTML report with scan results. - * - * @param webhookConfig The {@link LPVSQueue} configuration for the webhook. - * @param scanResults List containing preformatted scan results. - * @param conflicts List containing license conflict information. - * @return A string containing scan results in HTML format. - */ - public static String buildHTMLComment( - LPVSQueue webhookConfig, - List scanResults, - List> conflicts) { - StringBuilder htmlBuilder = new StringBuilder(); - - htmlBuilder.append(""); - - if (scanResults != null && scanResults.size() != 0) { - htmlBuilder.append("

Detected licenses:

"); - for (LPVSFile file : scanResults) { - htmlBuilder - .append("

File: ") - .append(file.getFilePath()) - .append("

"); - htmlBuilder - .append("

License(s): ") - .append(file.convertLicensesToString(LPVSVcs.GITHUB)) - .append("

"); - htmlBuilder - .append("

Component: ") - .append(file.getComponentName()) - .append(" (") - .append(file.getComponentFilePath()) - .append(")

"); - htmlBuilder - .append("

Matched Lines: ") - .append( - LPVSCommentUtil.getMatchedLinesAsLink( - webhookConfig, file, LPVSVcs.GITHUB)) - .append("

"); - htmlBuilder - .append("

Snippet Match: ") - .append(file.getSnippetMatch()) - .append("

"); - htmlBuilder.append("
"); - } - } - - if (conflicts != null && conflicts.size() > 0) { - htmlBuilder.append("

Detected license conflicts:

"); - htmlBuilder.append("
    "); - for (LPVSLicenseService.Conflict conflict : conflicts) { - htmlBuilder - .append("
  • ") - .append(conflict.l1) - .append(" and ") - .append(conflict.l2) - .append("
  • "); - } - htmlBuilder.append("
"); - if (webhookConfig.getHubLink() != null) { - htmlBuilder.append("

").append(webhookConfig.getHubLink()).append("

"); - } - } - - htmlBuilder.append(""); - - return htmlBuilder.toString(); - } - /** * Saves HTML report to given location. * @@ -221,7 +152,7 @@ public static void saveHTMLToFile(String htmlContent, String filePath) { writer.write(htmlContent); log.info("LPVS report saved to: " + filePath); } catch (IOException ex) { - log.error("error during saving HTML report: " + ex.getMessage()); + log.error("Error during saving HTML report: " + ex.getMessage()); } } } diff --git a/src/main/resources/templates/report_single_scan.html b/src/main/resources/templates/report_single_scan.html new file mode 100644 index 00000000..fa29e60f --- /dev/null +++ b/src/main/resources/templates/report_single_scan.html @@ -0,0 +1,141 @@ + + + + + + + + title + + + + + +
+

Report - License Pre-Validation Service (LPVS)


+
+
+ Scan date: scanDate
+ Source code location: codeLocation
+ Used scanner: usedScanner
+ Version of LPVS: lpvsVersion
+
+
+

Detected Licenses

+
+ Potential license problem(s) detected: licenseDetected +
    +
  • Prohibited license(s): prohibitedLicenses
  • +
  • Restricted license(s): restrictedLicenses
  • +
  • Unreviewed license(s): unreviewedLicenses
  • +
+
+
+ No license problems detected. +
+ +
+ +
+

+
+
+
+

Detected License Conflicts

+
+ Potential license conflict(s) detected: licenseConflicts +
+
+ No license conflicts detected. +
+
+ +
+

+
+
+ + + + diff --git a/src/test/java/com/lpvs/LicensePreValidationServiceTest.java b/src/test/java/com/lpvs/LicensePreValidationServiceTest.java index 87ce7f56..b775477f 100644 --- a/src/test/java/com/lpvs/LicensePreValidationServiceTest.java +++ b/src/test/java/com/lpvs/LicensePreValidationServiceTest.java @@ -7,6 +7,8 @@ package com.lpvs; import com.lpvs.util.LPVSExitHandler; +import org.apache.maven.model.Model; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,6 +20,8 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import java.io.FileReader; +import java.io.IOException; import java.lang.reflect.Field; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -144,4 +148,23 @@ public void testGetEmblem() { String emblem = LicensePreValidationService.getEmblem(); assertNotNull(emblem); } + + @Test + public void testGetVersion() throws Exception { + MavenXpp3Reader reader = Mockito.mock(MavenXpp3Reader.class); + Model model = new Model(); + model.setVersion("1.0"); + Mockito.when(reader.read(Mockito.any(FileReader.class))).thenReturn(model); + String version = LicensePreValidationService.getVersion(reader); + assertEquals("1.0", version); + } + + @Test + public void testGetVersionWithException() throws Exception { + MavenXpp3Reader reader = Mockito.mock(MavenXpp3Reader.class); + Mockito.when(reader.read(Mockito.any(FileReader.class))).thenThrow(new IOException()); + + String version = LicensePreValidationService.getVersion(reader); + assertEquals("latest", version); + } } diff --git a/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java b/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java new file mode 100644 index 00000000..d555f92b --- /dev/null +++ b/src/test/java/com/lpvs/entity/report/LPVSReportBuilderTest.java @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2024, Samsung Electronics Co., Ltd. All rights reserved. + * + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +package com.lpvs.entity.report; + +import com.lpvs.entity.LPVSFile; +import com.lpvs.entity.LPVSLicense; +import com.lpvs.service.LPVSLicenseService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.thymeleaf.TemplateEngine; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(SpringExtension.class) +@ImportAutoConfiguration(ThymeleafAutoConfiguration.class) +@ContextConfiguration(classes = {LPVSReportBuilder.class}) +@Slf4j +public class LPVSReportBuilderTest { + + @Autowired private TemplateEngine templateEngine; + + LPVSFile file1, file2, file3, file4; + LPVSLicense lic1, lic2, lic3, lic4; + LPVSLicenseService.Conflict conflict1, conflict2; + LPVSReportBuilder reportBuilder; + + @BeforeEach + public void setUp() throws Exception { + lic1 = + new LPVSLicense() { + { + setLicenseName("MIT License"); + setAccess("PERMITTED"); + setSpdxId("MIT"); + } + }; + lic2 = + new LPVSLicense() { + { + setLicenseName("GNU General Public License v3.0 only"); + setAccess("PROHIBITED"); + setSpdxId("GPL-3.0-only"); + } + }; + lic3 = + new LPVSLicense() { + { + setLicenseName("GNU Lesser General Public License v2.0 or later"); + setAccess("RESTRICTED"); + setSpdxId("LGPL-2.0-or-later"); + } + }; + lic4 = + new LPVSLicense() { + { + setLicenseName("Apache License 2.0"); + setAccess("UNREVIEWED"); + setSpdxId("Apache-2.0"); + } + }; + + file1 = new LPVSFile(); + file1.setLicenses( + new HashSet<>() { + { + add(lic1); + } + }); + file1.setFilePath("local_file_path_1"); + file1.setComponentFilePath("component_file_path_1"); + file1.setComponentFileUrl("http://component_name_1/file_url"); + file1.setComponentName("component_name_1"); + file1.setComponentUrl("http://component_name_1/url"); + file1.setComponentVersion("v1.0.0"); + file1.setComponentVendor("component_vendor_1"); + file1.setSnippetMatch("80%"); + file1.setMatchedLines("5-17"); + + file2 = new LPVSFile(); + file2.setLicenses( + new HashSet<>() { + { + add(lic2); + add(lic3); + } + }); + file2.setFilePath("local_file_path_2"); + file2.setComponentFilePath("component_file_path_2"); + file2.setComponentName("component_name_2"); + file2.setComponentUrl("http://component_name_2/url"); + file2.setComponentVersion("v2.0.0"); + file2.setComponentVendor("component_vendor_2"); + file2.setSnippetMatch("100%"); + file2.setMatchedLines("all"); + + file3 = new LPVSFile(); + file3.setLicenses( + new HashSet<>() { + { + add(lic4); + } + }); + file3.setFilePath("local_file_path_3"); + file3.setComponentFilePath("component_file_path_3"); + file3.setComponentFileUrl("http://component_name_3/file_url"); + file3.setComponentName("component_name_3"); + file3.setComponentUrl("http://component_name_3/url"); + file3.setComponentVersion("v3.0.0"); + file3.setComponentVendor("component_vendor_3"); + file3.setSnippetMatch("20%"); + file3.setMatchedLines("1-10"); + + file4 = new LPVSFile(); + file4.setLicenses( + new HashSet<>() { + { + add(lic4); + } + }); + file4.setFilePath("local_file_path_4"); + file4.setComponentFilePath("component_file_path_4"); + file4.setComponentName("component_name_4"); + file4.setComponentUrl("http://component_name_4/url"); + file4.setComponentVersion("v4.0.0"); + file4.setComponentVendor("component_vendor_4"); + file4.setSnippetMatch("50%"); + file4.setMatchedLines("5-10"); + + conflict1 = new LPVSLicenseService.Conflict<>("GPL-3.0-only", "Apache-2.0"); + conflict2 = new LPVSLicenseService.Conflict<>("LGPL-2.0-or-later", "MIT"); + + reportBuilder = new LPVSReportBuilder(templateEngine); + } + + private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + + @Test + public void testGenerateHtmlReportSingleScan_Empty() { + String actual = reportBuilder.generateHtmlReportSingleScan("some/path", null, null); + assertThat(actual).contains(sdf.format(new Date())); // check title and scanDate + assertThat(actual).contains("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithLicensesAndConflicts() { + List scanResults = List.of(file1, file2, file3, file4); + List> conflicts = List.of(conflict1, conflict2); + String actual = + reportBuilder.generateHtmlReportSingleScan("some/path", scanResults, conflicts); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).doesNotContain("No license problems detected"); + assertThat(actual).doesNotContain("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithLicenses() { + List scanResults = List.of(file3, file4); + String actual = reportBuilder.generateHtmlReportSingleScan("some/path", scanResults, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).doesNotContain("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } + + @Test + public void testGenerateHtmlReportSingleScan_WithPermittedLicenses() { + List scanResults = List.of(file1); + String actual = reportBuilder.generateHtmlReportSingleScan("some/path", scanResults, null); + assertThat(actual).contains(sdf.format(new Date())); + assertThat(actual).contains("No license problems detected"); + assertThat(actual).contains("No license conflicts detected"); + } +} diff --git a/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java b/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java index 6fd0c860..d18efc62 100644 --- a/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java +++ b/src/test/java/com/lpvs/service/scan/LPVSDetectServiceTest.java @@ -9,6 +9,7 @@ import com.lpvs.entity.LPVSFile; import com.lpvs.entity.LPVSLicense; import com.lpvs.entity.LPVSQueue; +import com.lpvs.entity.report.LPVSReportBuilder; import com.lpvs.service.LPVSGitHubConnectionService; import com.lpvs.service.LPVSGitHubService; import com.lpvs.service.LPVSLicenseService; @@ -60,6 +61,7 @@ class TestRunScan__Scanoss { LPVSLicenseService licenseservice_mock = mock(LPVSLicenseService.class); LPVSGitHubService githubservice_mock = mock(LPVSGitHubService.class); LPVSScanServiceFactory scanServiceFactory_mock = mock(LPVSScanServiceFactory.class); + LPVSReportBuilder reportBuilder_mock = mock(LPVSReportBuilder.class); GitHub mockGitHub = mock(GitHub.class); GHCommitPointer mockCommitPointer = mock(GHCommitPointer.class); GHRepository mockRepository = mock(GHRepository.class); @@ -82,7 +84,8 @@ void setUp() throws IOException { github_mock, licenseservice_mock, githubservice_mock, - scanServiceFactory_mock); + scanServiceFactory_mock, + reportBuilder_mock); webhookConfig = new LPVSQueue(); webhookConfig.setId(1L); @@ -149,7 +152,13 @@ void testRunOneScanPullRequestWithNullTrigger() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "trigger", null); @@ -166,7 +175,13 @@ void testRunOneScanLocalFileWithNullTrigger() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "localPath", null); @@ -183,7 +198,13 @@ void testRunOneScanBothPullRequestAndLocalFile() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); setPrivateField(lpvsDetectService, "trigger", ""); @@ -198,7 +219,13 @@ void testRunOneScanBothPullRequestAndLocalFile2() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); setPrivateField(lpvsDetectService, "trigger", "some-pull-request"); @@ -214,7 +241,13 @@ void testRunOneScan_PullRequest_Default() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(lpvsDetectService, "trigger", "fake-trigger-value"); setPrivateField(lpvsDetectService, "ctx", mockApplicationContext); @@ -235,7 +268,13 @@ void testRunOneScan_PullRequest_Branch2() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); // Mock the necessary GitHub objects for LPVSQueue when(mockGitHub.getRepository(any())).thenReturn(mockRepository); @@ -284,7 +323,13 @@ void testRunOneScan_Branch3() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); setPrivateField(detectService, "trigger", "github/owner/repo/branch/123"); setPrivateField(detectService, "htmlReport", "build"); @@ -302,6 +347,8 @@ void testRunOneScan_Branch3() .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -331,7 +378,13 @@ void testRunOneScan_LocalFiles_WithConsoleReport() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -381,7 +434,13 @@ void testRunOneScan_LocalFiles_WithHtmlReport() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -407,6 +466,8 @@ void testRunOneScan_LocalFiles_WithHtmlReport() .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -437,7 +498,13 @@ void testRunOneScan_LocalFiles_NoFile() lpvsDetectService = spy( new LPVSDetectService( - "scanoss", false, null, null, null, scanServiceFactory_mock)); + "scanoss", + false, + null, + null, + null, + scanServiceFactory_mock, + null)); File sourceDir = Files.createTempDirectory("source").toFile(); File sourceFile1 = new File(sourceDir, "file1.txt"); @@ -511,6 +578,8 @@ void testRunOneScan_TriggerNotNull() throws Exception { .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -555,6 +624,8 @@ void testRunOneScan_TriggerNotNull_Branch2() throws Exception { .thenReturn(new URL("https://example.com/repo/files")); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -598,6 +669,8 @@ void testRunOneScan_TriggerNotNull_Branch3() throws Exception { when(mockCommitPointer.getRepository()).thenReturn(mockHeadRepository2); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -639,6 +712,8 @@ void testRunOneScan_TriggerNotNull_NoDirectory() throws Exception { when(mockCommitPointer.getRepository()).thenReturn(mockHeadRepository2); when(githubservice_mock.getInternalQueueByPullRequest(anyString())) .thenReturn(webhookConfig); + when(reportBuilder_mock.generateHtmlReportSingleScan(anyString(), anyList(), anyList())) + .thenReturn(""); // Set up expected values String expectedPullRequestUrl = "https://example.com/pull/1"; @@ -667,11 +742,8 @@ void testCommentBuilder_ConflictFilePresent() { List scanResults = new ArrayList<>(); String commentGitHub = LPVSCommentUtil.reportCommentBuilder(webhookConfig, scanResults, expected); - String commentHTML = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, expected); assertNotNull(commentGitHub); - assertNotNull(commentHTML); } @Test @@ -681,11 +753,8 @@ void testCommentBuilder_NoConflictNoLicense() { List scanResults = new ArrayList<>(); String commentGitHub = LPVSCommentUtil.reportCommentBuilder(webhookConfig, scanResults, expected); - String commentHTML = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, expected); assertEquals(commentGitHub, ""); - assertEquals(commentHTML, ""); } @Test @@ -737,6 +806,7 @@ class TestRunScan__ScanossException { LPVSLicenseService licenseservice_mock = mock(LPVSLicenseService.class); LPVSGitHubService githubservice_mock = mock(LPVSGitHubService.class); LPVSScanServiceFactory scanServiceFactory_mock = mock(LPVSScanServiceFactory.class); + LPVSReportBuilder reportBuilder_mock = mock(LPVSReportBuilder.class); LPVSQueue webhookConfig; final String test_path = "test_path"; @@ -751,7 +821,8 @@ void setUp() { github_mock, licenseservice_mock, githubservice_mock, - scanServiceFactory_mock); + scanServiceFactory_mock, + reportBuilder_mock); webhookConfig = new LPVSQueue(); webhookConfig.setId(1L); @@ -801,7 +872,7 @@ class TestRunScan__NotScanoss { void setUp() { detectService = new LPVSDetectService( - "not_scanoss", false, null, null, null, scanServiceFactory_mock); + "not_scanoss", false, null, null, null, scanServiceFactory_mock, null); } @Test diff --git a/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java b/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java index c94a99d0..3429c641 100644 --- a/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java +++ b/src/test/java/com/lpvs/util/LPVSCommentUtilTest.java @@ -153,35 +153,6 @@ void testReportCommentBuilder_HubLink() { assertNotNull(comment); } - @Test - void testBuildHTMLComment() { - LPVSQueue webhookConfig = new LPVSQueue(); - List scanResults = new ArrayList<>(); - scanResults.add(createSampleFile("testPath1", "test1")); - List> conflicts = new ArrayList<>(); - - String htmlComment = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, conflicts); - - assertNotNull(htmlComment); - } - - @Test - void testBuildHTMLComment_HubLink() { - LPVSQueue webhookConfig = new LPVSQueue(); - List scanResults = new ArrayList<>(); - scanResults.add(createSampleFile("testPath1", "test1")); - LPVSLicenseService.Conflict conflict_1 = - new LPVSLicenseService.Conflict<>("MIT", "Apache-2.0"); - List> conflicts = - List.of(conflict_1, conflict_1); - webhookConfig.setHubLink("some_link"); - String htmlComment = - LPVSCommentUtil.buildHTMLComment(webhookConfig, scanResults, conflicts); - - assertNotNull(htmlComment); - } - @Test void testSaveHTMLToFile() throws IOException { String htmlContent = "

Test HTML

"; From 97921cf4095cf30bf8c9455070b3faca2fe8f183 Mon Sep 17 00:00:00 2001 From: Oleg Kopysov Date: Wed, 14 Aug 2024 18:48:36 +0300 Subject: [PATCH 2/2] fix: Use old-style switch to avoid code style error Signed-off-by: Oleg Kopysov --- .../lpvs/entity/report/LPVSReportBuilder.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java index d4a42be8..2e4de027 100644 --- a/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java +++ b/src/main/java/com/lpvs/entity/report/LPVSReportBuilder.java @@ -451,19 +451,18 @@ private String generateLicenseConflictsTable( * @return the explanation for the specified license type */ private String getExplanationForLicenseType(String type) { - return switch (type.toUpperCase()) { - case prohibited -> - "This license prohibits the use of the licensed code in certain contexts, such as commercial software development."; - case restricted -> - "This license required compliance with specific obligations. It is crucial to carefully review and adhere to these obligations before using the licensed code."; - case unreviewed -> - "This license has not been reviewed thoroughly and may contain unknown risks or limitations. It is recommended to review these licenses carefully before using the licensed code."; - case permitted -> - "This license permits free usage, modification, and distribution of the licensed code without any restrictions."; - default -> - throw new IllegalStateException( - "Unexpected value for the license type: " + type); - }; + switch (type.toUpperCase()) { + case prohibited: + return "This license prohibits the use of the licensed code in certain contexts, such as commercial software development."; + case restricted: + return "This license required compliance with specific obligations. It is crucial to carefully review and adhere to these obligations before using the licensed code."; + case unreviewed: + return "This license has not been reviewed thoroughly and may contain unknown risks or limitations. It is recommended to review these licenses carefully before using the licensed code."; + case permitted: + return "This license permits free usage, modification, and distribution of the licensed code without any restrictions."; + default: + throw new IllegalStateException("Unexpected value for the license type: " + type); + } } /**