diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java index 75bbaf826b00..39b3397b578b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobExecutionService.java @@ -378,18 +378,18 @@ private BuildResult parseTestResults(TarArchiveInputStream testResultsTarInputSt } // Read the contents of the tar entry as a string. - String xmlString = readTarEntryContent(testResultsTarInputStream); + String fileString = readTarEntryContent(testResultsTarInputStream); // Get the file name of the tar entry. String fileName = getFileName(tarEntry); try { // Check if the file is a static code analysis report file if (StaticCodeAnalysisTool.getToolByFilePattern(fileName).isPresent()) { - processStaticCodeAnalysisReportFile(fileName, xmlString, staticCodeAnalysisReports, buildJobId); + processStaticCodeAnalysisReportFile(fileName, fileString, staticCodeAnalysisReports, buildJobId); } else { // ugly workaround because in swift result files \n\t breaks the parsing - var testResultFileString = xmlString.replace("\n\t", ""); + var testResultFileString = fileString.replace("\n\t", ""); if (!testResultFileString.isBlank()) { processTestResultFile(testResultFileString, failedTests, successfulTests); } @@ -418,7 +418,7 @@ private boolean isValidTestResultFile(TarArchiveEntry tarArchiveEntry) { String result = (lastIndexOfSlash != -1 && lastIndexOfSlash + 1 < name.length()) ? name.substring(lastIndexOfSlash + 1) : name; // Java test result files are named "TEST-*.xml", Python test result files are named "*results.xml". - return !tarArchiveEntry.isDirectory() && result.endsWith(".xml") && !result.equals("pom.xml"); + return !tarArchiveEntry.isDirectory() && (result.endsWith(".xml") && !result.equals("pom.xml") || result.endsWith(".sarif")); } /** @@ -444,12 +444,12 @@ private String getFileName(TarArchiveEntry tarEntry) { * Processes a static code analysis report file and adds the report to the corresponding list. * * @param fileName the file name of the static code analysis report file - * @param xmlString the content of the static code analysis report file + * @param reportContent the content of the static code analysis report file * @param staticCodeAnalysisReports the list of static code analysis reports */ - private void processStaticCodeAnalysisReportFile(String fileName, String xmlString, List staticCodeAnalysisReports, String buildJobId) { + private void processStaticCodeAnalysisReportFile(String fileName, String reportContent, List staticCodeAnalysisReports, String buildJobId) { try { - staticCodeAnalysisReports.add(ReportParser.getReport(xmlString, fileName)); + staticCodeAnalysisReports.add(ReportParser.getReport(reportContent, fileName)); } catch (UnsupportedToolException e) { String msg = "Failed to parse static code analysis report for " + fileName; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/StaticCodeAnalysisTool.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/StaticCodeAnalysisTool.java index 9068d7fadd9c..dd3ffcbea67a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/StaticCodeAnalysisTool.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/StaticCodeAnalysisTool.java @@ -1,7 +1,8 @@ package de.tum.cit.aet.artemis.programming.domain; -import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -10,28 +11,29 @@ */ public enum StaticCodeAnalysisTool { - SPOTBUGS(ProgrammingLanguage.JAVA, "spotbugs:spotbugs", "spotbugsXml.xml"), CHECKSTYLE(ProgrammingLanguage.JAVA, "checkstyle:checkstyle", "checkstyle-result.xml"), - PMD(ProgrammingLanguage.JAVA, "pmd:pmd", "pmd.xml"), PMD_CPD(ProgrammingLanguage.JAVA, "pmd:cpd", "cpd.xml"), SWIFTLINT(ProgrammingLanguage.SWIFT, "", "swiftlint-result.xml"), - GCC(ProgrammingLanguage.C, "", "gcc.xml"); + // @formatter:off + SPOTBUGS("spotbugsXml.xml"), + CHECKSTYLE("checkstyle-result.xml"), + PMD("pmd.xml"), + PMD_CPD("cpd.xml"), + SWIFTLINT("swiftlint-result.xml"), + GCC("gcc.xml"), + OTHER(null), + ; + // @formatter:on - private final ProgrammingLanguage language; + // @formatter:off + private static final Map> TOOLS_OF_PROGRAMMING_LANGUAGE = new EnumMap<>(Map.of( + ProgrammingLanguage.JAVA, List.of(SPOTBUGS, CHECKSTYLE, PMD, PMD_CPD), + ProgrammingLanguage.SWIFT, List.of(SWIFTLINT), + ProgrammingLanguage.C, List.of(GCC) + )); + // @formatter:on - private final String command; + private final String fileName; - private final String filePattern; - - StaticCodeAnalysisTool(ProgrammingLanguage language, String command, String filePattern) { - this.language = language; - this.command = command; - this.filePattern = filePattern; - } - - public String getTask() { - return this.command; - } - - public String getFilePattern() { - return this.filePattern; + StaticCodeAnalysisTool(String fileName) { + this.fileName = fileName; } /** @@ -41,13 +43,7 @@ public String getFilePattern() { * @return List of static code analysis */ public static List getToolsForProgrammingLanguage(ProgrammingLanguage language) { - List tools = new ArrayList<>(); - for (var tool : StaticCodeAnalysisTool.values()) { - if (tool.language == language) { - tools.add(tool); - } - } - return tools; + return TOOLS_OF_PROGRAMMING_LANGUAGE.getOrDefault(language, List.of()); } /** @@ -58,7 +54,7 @@ public static List getToolsForProgrammingLanguage(Progra */ public static Optional getToolByFilePattern(String fileName) { for (StaticCodeAnalysisTool tool : StaticCodeAnalysisTool.values()) { - if (Objects.equals(fileName, tool.filePattern)) { + if (Objects.equals(fileName, tool.fileName)) { return Optional.of(tool); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java index 9b8f59766ac2..f31cbcd0c63e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/ReportParser.java @@ -1,94 +1,27 @@ package de.tum.cit.aet.artemis.programming.service.localci.scaparser; -import static de.tum.cit.aet.artemis.programming.service.localci.scaparser.utils.ReportUtils.createErrorReport; -import static de.tum.cit.aet.artemis.programming.service.localci.scaparser.utils.ReportUtils.createFileTooLargeReport; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; - -import com.fasterxml.jackson.databind.ObjectMapper; - import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; -import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.ParserException; import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.UnsupportedToolException; import de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.ParserPolicy; import de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.ParserStrategy; -import de.tum.cit.aet.artemis.programming.service.localci.scaparser.utils.FileUtils; /** * Public API for parsing of static code analysis reports */ public class ReportParser { - private final ObjectMapper mapper = new ObjectMapper(); - - // Reports that are bigger then the threshold will not be parsed - // and an issue will be generated. The unit is in megabytes. - private static final int STATIC_CODE_ANALYSIS_REPORT_FILESIZE_LIMIT_IN_MB = 1; - - /** - * Transform a given static code analysis report into a JSON representation. - * All supported tools share the same JSON format. - * - * @param file Reference to the static code analysis report - * @return Static code analysis report represented as a JSON String - * @throws ParserException - If an exception occurs that is not already handled by the parser itself, e.g. caused by the json-parsing - */ - public String transformToJSONReport(File file) throws ParserException { - try { - StaticCodeAnalysisReportDTO report = transformToReport(file); - return mapper.writeValueAsString(report); - } - catch (Exception e) { - throw new ParserException(e.getMessage(), e); - } - } - - /** - * Transform a given static code analysis report given as a file into a plain Java object. - * - * @param file Reference to the static code analysis report - * @return Static code analysis report represented as a plain Java object - */ - public StaticCodeAnalysisReportDTO transformToReport(File file) { - if (file == null) { - throw new IllegalArgumentException("File must not be null"); - } - - // The static code analysis parser only supports xml files. - if (!FileUtils.getExtension(file).equals("xml")) { - throw new IllegalArgumentException("File must be xml format"); - } - try { - // Reject any file larger than the given threshold - if (FileUtils.isFilesizeGreaterThan(file, STATIC_CODE_ANALYSIS_REPORT_FILESIZE_LIMIT_IN_MB)) { - return createFileTooLargeReport(file.getName()); - } - - return getReport(file); - } - catch (Exception e) { - return createErrorReport(file.getName(), e); - } - } + private static final ParserPolicy parserPolicy = new ParserPolicy(); /** - * Builds the document using the provided file and parses it to a Report object using ObjectMapper. + * Builds the document using the provided string and parses it to a Report object. * - * @param file File referencing the static code analysis report + * @param reportContent String containing the static code analysis report + * @param fileName filename of the report used for configuring a parser * @return Report containing the static code analysis issues * @throws UnsupportedToolException if the static code analysis tool which created the report is not supported - * @throws IOException if the file could not be read */ - public static StaticCodeAnalysisReportDTO getReport(File file) throws IOException { - String xmlContent = Files.readString(file.toPath()); - return getReport(xmlContent, file.getName()); - } - - public static StaticCodeAnalysisReportDTO getReport(String xmlContent, String fileName) { - ParserPolicy parserPolicy = new ParserPolicy(); + public static StaticCodeAnalysisReportDTO getReport(String reportContent, String fileName) { ParserStrategy parserStrategy = parserPolicy.configure(fileName); - return parserStrategy.parse(xmlContent); + return parserStrategy.parse(reportContent); } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/exception/ParserException.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/exception/ParserException.java deleted file mode 100644 index f0934ad041f3..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/exception/ParserException.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception; - -/** - * Exception thrown when an error occurs during parsing. - */ -public class ParserException extends Exception { - - /** - * Creates a new ParserException. - * - * @param message the detail message. - */ - public ParserException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ArtifactLocation.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ArtifactLocation.java new file mode 100644 index 000000000000..ab497fc272d9 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ArtifactLocation.java @@ -0,0 +1,22 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Specifies the location of an artifact. + * + * @param uri A string containing a valid relative or absolute URI. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ArtifactLocation(String uri) { + + /** + * A string containing a valid relative or absolute URI. + */ + public Optional getOptionalUri() { + return Optional.ofNullable(uri); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/GlobalMessageStrings.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/GlobalMessageStrings.java new file mode 100644 index 000000000000..04293695820b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/GlobalMessageStrings.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; + +/** + * A dictionary, each of whose keys is a resource identifier and each of whose values is a multiformatMessageString object, which holds message strings in plain text and + * (optionally) Markdown format. The strings can include placeholders, which can be used to construct a message in combination with an arbitrary number of additional string + * arguments. + */ +public record GlobalMessageStrings(@JsonAnySetter Map additionalProperties) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Location.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Location.java new file mode 100644 index 000000000000..20dbd72ef99d --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Location.java @@ -0,0 +1,23 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A location within a programming artifact. + * + * @param physicalLocation A physical location relevant to a result. Specifies a reference to a programming artifact together with a range of bytes or characters within that + * artifact. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Location(PhysicalLocation physicalLocation) { + + /** + * A physical location relevant to a result. Specifies a reference to a programming artifact together with a range of bytes or characters within that artifact. + */ + public Optional getOptionalPhysicalLocation() { + return Optional.ofNullable(physicalLocation); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Message.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Message.java new file mode 100644 index 000000000000..d353df2e203e --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Message.java @@ -0,0 +1,30 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Encapsulates a message intended to be read by the end user. + * + * @param text A plain text message string. + * @param id The identifier for this message. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Message(String text, String id) { + + /** + * A plain text message string. + */ + public Optional getOptionalText() { + return Optional.ofNullable(text); + } + + /** + * The identifier for this message. + */ + public Optional getOptionalId() { + return Optional.ofNullable(id); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MessageStrings.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MessageStrings.java new file mode 100644 index 000000000000..a888c306bcee --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MessageStrings.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; + +/** + * A set of name/value pairs with arbitrary names. Each value is a multiformatMessageString object, which holds message strings in plain text and (optionally) Markdown format. The + * strings can include placeholders, which can be used to construct a message in combination with an arbitrary number of additional string arguments. + */ +public record MessageStrings(@JsonAnySetter Map additionalProperties) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MultiformatMessageString.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MultiformatMessageString.java new file mode 100644 index 000000000000..a08afe804d02 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/MultiformatMessageString.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A message string or message format string rendered in multiple formats. + * + * @param text A plain text message string or format string. + * (Required) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record MultiformatMessageString(String text) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PhysicalLocation.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PhysicalLocation.java new file mode 100644 index 000000000000..b122875e94be --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PhysicalLocation.java @@ -0,0 +1,30 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A physical location relevant to a result. Specifies a reference to a programming artifact together with a range of bytes or characters within that artifact. + * + * @param artifactLocation Specifies the location of an artifact. + * @param region A region within an artifact where a result was detected. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record PhysicalLocation(ArtifactLocation artifactLocation, Region region) { + + /** + * Specifies the location of an artifact. + */ + public Optional getOptionalArtifactLocation() { + return Optional.ofNullable(artifactLocation); + } + + /** + * A region within an artifact where a result was detected. + */ + public Optional getOptionalRegion() { + return Optional.ofNullable(region); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PropertyBag.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PropertyBag.java new file mode 100644 index 000000000000..ba3b0bb208fb --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/PropertyBag.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Key/value pairs that provide additional information about the object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record PropertyBag(@JsonAnySetter Map additionalProperties) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Region.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Region.java new file mode 100644 index 000000000000..f9ac3391df87 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Region.java @@ -0,0 +1,46 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A region within an artifact where a result was detected. + * + * @param startLine The line number of the first character in the region. + * @param startColumn The column number of the first character in the region. + * @param endLine The line number of the last character in the region. + * @param endColumn The column number of the character following the end of the region. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Region(Integer startLine, Integer startColumn, Integer endLine, Integer endColumn) { + + /** + * The line number of the first character in the region. + */ + public Optional getOptionalStartLine() { + return Optional.ofNullable(startLine); + } + + /** + * The column number of the first character in the region. + */ + public Optional getOptionalStartColumn() { + return Optional.ofNullable(startColumn); + } + + /** + * The line number of the last character in the region. + */ + public Optional getOptionalEndLine() { + return Optional.ofNullable(endLine); + } + + /** + * The column number of the character following the end of the region. + */ + public Optional getOptionalEndColumn() { + return Optional.ofNullable(endColumn); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptor.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptor.java new file mode 100644 index 000000000000..75552ee32d69 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptor.java @@ -0,0 +1,112 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.net.URI; +import java.util.Optional; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Metadata that describes a specific report produced by the tool, as part of the analysis it provides or its runtime reporting. + * + * @param id A stable, opaque identifier for the report. + * (Required) + * @param deprecatedIds An array of stable, opaque identifiers by which this report was known in some previous version of the analysis tool. + * @param guid A unique identifier for the reporting descriptor in the form of a GUID. + * @param deprecatedGuids An array of unique identifies in the form of a GUID by which this report was known in some previous version of the analysis tool. + * @param name A report identifier that is understandable to an end user. + * @param deprecatedNames An array of readable identifiers by which this report was known in some previous version of the analysis tool. + * @param shortDescription A message string or message format string rendered in multiple formats. + * @param fullDescription A message string or message format string rendered in multiple formats. + * @param messageStrings A set of name/value pairs with arbitrary names. Each value is a multiformatMessageString object, which holds message strings in plain text and + * (optionally) Markdown format. + * The strings can include placeholders, which can be used to construct a message in combination with an arbitrary number of additional string + * arguments. + * @param helpUri A URI where the primary documentation for the report can be found. + * @param help A message string or message format string rendered in multiple formats. + * @param properties Key/value pairs that provide additional information about the object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ReportingDescriptor(String id, Set deprecatedIds, String guid, Set deprecatedGuids, String name, Set deprecatedNames, + MultiformatMessageString shortDescription, MultiformatMessageString fullDescription, MessageStrings messageStrings, URI helpUri, MultiformatMessageString help, + PropertyBag properties) { + + /** + * An array of stable, opaque identifiers by which this report was known in some previous version of the analysis tool. + */ + public Optional> getOptionalDeprecatedIds() { + return Optional.ofNullable(deprecatedIds); + } + + /** + * A unique identifier for the reporting descriptor in the form of a GUID. + */ + public Optional getOptionalGuid() { + return Optional.ofNullable(guid); + } + + /** + * An array of unique identifies in the form of a GUID by which this report was known in some previous version of the analysis tool. + */ + public Optional> getOptionalDeprecatedGuids() { + return Optional.ofNullable(deprecatedGuids); + } + + /** + * A report identifier that is understandable to an end user. + */ + public Optional getOptionalName() { + return Optional.ofNullable(name); + } + + /** + * An array of readable identifiers by which this report was known in some previous version of the analysis tool. + */ + public Optional> getOptionalDeprecatedNames() { + return Optional.ofNullable(deprecatedNames); + } + + /** + * A message string or message format string rendered in multiple formats. + */ + public Optional getOptionalShortDescription() { + return Optional.ofNullable(shortDescription); + } + + /** + * A message string or message format string rendered in multiple formats. + */ + public Optional getOptionalFullDescription() { + return Optional.ofNullable(fullDescription); + } + + /** + * A set of name/value pairs with arbitrary names. Each value is a multiformatMessageString object, which holds message strings in plain text and (optionally) Markdown format. + * The strings can include placeholders, which can be used to construct a message in combination with an arbitrary number of additional string arguments. + */ + public Optional getOptionalMessageStrings() { + return Optional.ofNullable(messageStrings); + } + + /** + * A URI where the primary documentation for the report can be found. + */ + public Optional getOptionalHelpUri() { + return Optional.ofNullable(helpUri); + } + + /** + * A message string or message format string rendered in multiple formats. + */ + public Optional getOptionalHelp() { + return Optional.ofNullable(help); + } + + /** + * Key/value pairs that provide additional information about the object. + */ + public Optional getOptionalProperties() { + return Optional.ofNullable(properties); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptorReference.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptorReference.java new file mode 100644 index 000000000000..f46e722e6b1b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ReportingDescriptorReference.java @@ -0,0 +1,37 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Information about how to locate a relevant reporting descriptor. + * + * @param id The id of the descriptor. + * @param index The index into an array of descriptors in toolComponent.ruleDescriptors, toolComponent.notificationDescriptors, or toolComponent.taxonomyDescriptors, depending on + * context. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ReportingDescriptorReference(String id, Integer index) { + + public ReportingDescriptorReference(String id, Integer index) { + this.id = id; + this.index = Objects.requireNonNullElse(index, -1); + } + + /** + * The id of the descriptor. + */ + public Optional getOptionalId() { + return Optional.ofNullable(id); + } + + /** + * The index into an array of descriptors in toolComponent.ruleDescriptors, toolComponent.notificationDescriptors, or toolComponent.taxonomyDescriptors, depending on context. + */ + public Optional getOptionalIndex() { + return Optional.ofNullable(index); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Result.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Result.java new file mode 100644 index 000000000000..49d17e49649a --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Result.java @@ -0,0 +1,171 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * A result produced by an analysis tool. + * + * @param ruleId The stable, unique identifier of the rule, if any, to which this result is relevant. + * @param ruleIndex The index within the tool component rules array of the rule object associated with this result. + * @param rule Information about how to locate a relevant reporting descriptor. + * @param kind A value that categorizes results by evaluation state. + * @param level A value specifying the severity level of the result. + * @param message A message that describes the result. The first sentence of the message only will be displayed when visible space is limited. + * @param locations The set of locations where the result was detected. Specify only one location unless the problem indicated by the result can only be corrected by making a + * change at every specified location. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Result(String ruleId, Integer ruleIndex, ReportingDescriptorReference rule, + de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Result.Kind kind, + de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Result.Level level, Message message, List locations) { + + public Result(String ruleId, Integer ruleIndex, ReportingDescriptorReference rule, Kind kind, Level level, Message message, List locations) { + this.ruleId = ruleId; + this.ruleIndex = Objects.requireNonNullElse(ruleIndex, -1); + this.rule = rule; + this.kind = Objects.requireNonNullElse(kind, Kind.FAIL); + this.level = Objects.requireNonNullElse(level, Level.WARNING); + this.message = message; + this.locations = locations; + } + + /** + * The stable, unique identifier of the rule, if any, to which this result is relevant. + */ + public Optional getOptionalRuleId() { + return Optional.ofNullable(ruleId); + } + + /** + * The index within the tool component rules array of the rule object associated with this result. + */ + public Optional getOptionalRuleIndex() { + return Optional.ofNullable(ruleIndex); + } + + /** + * Information about how to locate a relevant reporting descriptor. + */ + public Optional getOptionalRule() { + return Optional.ofNullable(rule); + } + + /** + * A value that categorizes results by evaluation state. + */ + public Optional getOptionalKind() { + return Optional.ofNullable(kind); + } + + /** + * A value specifying the severity level of the result. + */ + public Optional getOptionalLevel() { + return Optional.ofNullable(level); + } + + /** + * The set of locations where the result was detected. Specify only one location unless the problem indicated by the result can only be corrected by making a change at every + * specified location. + */ + public Optional> getOptionalLocations() { + return Optional.ofNullable(locations); + } + + /** + * A value that categorizes results by evaluation state. + */ + public enum Kind { + + NOT_APPLICABLE("notApplicable"), PASS("pass"), FAIL("fail"), REVIEW("review"), OPEN("open"), INFORMATIONAL("informational"); + + private final String value; + + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (Kind c : values()) { + CONSTANTS.put(c.value, c); + } + } + + Kind(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + @JsonValue + public String value() { + return this.value; + } + + @JsonCreator + public static Kind fromValue(String value) { + Kind constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } + else { + return constant; + } + } + + } + + /** + * A value specifying the severity level of the result. + */ + public enum Level { + + NONE("none"), NOTE("note"), WARNING("warning"), ERROR("error"); + + private final String value; + + private final static Map CONSTANTS = new HashMap<>(); + + static { + for (Level c : values()) { + CONSTANTS.put(c.value, c); + } + } + + Level(String value) { + this.value = value; + } + + @Override + public String toString() { + return this.value; + } + + @JsonValue + public String value() { + return this.value; + } + + @JsonCreator + public static Level fromValue(String value) { + Level constant = CONSTANTS.get(value); + if (constant == null) { + throw new IllegalArgumentException(value); + } + else { + return constant; + } + } + + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Run.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Run.java new file mode 100644 index 000000000000..c77d9ccaee4b --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Run.java @@ -0,0 +1,27 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Describes a single run of an analysis tool, and contains the reported output of that run. + * + * @param tool The analysis tool that was run. + * (Required) + * @param results The set of results contained in an SARIF log. The results array can be omitted when a run is solely exporting rules metadata. It must be present (but may be + * empty) if a log file represents an actual scan. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Run(Tool tool, List results) { + + /** + * The set of results contained in an SARIF log. The results array can be omitted when a run is solely exporting rules metadata. It must be present (but may be empty) if a log + * file represents an actual scan. + */ + public Optional> getOptionalResults() { + return Optional.ofNullable(results); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/SarifLog.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/SarifLog.java new file mode 100644 index 000000000000..28efb3370c53 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/SarifLog.java @@ -0,0 +1,18 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * Static Analysis Results Format (SARIF) Version 2.1.0 JSON Schema + *

+ * Static Analysis Results Format (SARIF) Version 2.1.0 JSON Schema: a standard format for the output of static analysis tools. + * + * @param runs The set of runs contained in this log file. + * (Required) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record SarifLog(List runs) { + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Tool.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Tool.java new file mode 100644 index 000000000000..d6af4c944ba3 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/Tool.java @@ -0,0 +1,13 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * The analysis tool that was run. + * + * @param driver A component, such as a plug-in or the driver, of the analysis tool that was run. + * (Required) + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record Tool(ToolComponent driver) { +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ToolComponent.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ToolComponent.java new file mode 100644 index 000000000000..b7fb4cf33d3e --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/format/sarif/ToolComponent.java @@ -0,0 +1,37 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif; + +import java.util.List; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * A component, such as a plug-in or the driver, of the analysis tool that was run. + * + * @param name The name of the tool component. + * (Required) + * @param globalMessageStrings A dictionary, each of whose keys is a resource identifier and each of whose values is a multiformatMessageString object, which holds message strings + * in plain text and (optionally) Markdown format. The strings can include placeholders, which can be used to construct a message in combination + * with an arbitrary number of additional string arguments. + * @param rules An array of reportingDescriptor objects relevant to the analysis performed by the tool component. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolComponent(String name, GlobalMessageStrings globalMessageStrings, List rules) { + + /** + * A dictionary, each of whose keys is a resource identifier and each of whose values is a multiformatMessageString object, which holds message strings in plain text and + * (optionally) Markdown format. The strings can include placeholders, which can be used to construct a message in combination with an arbitrary number of additional string + * arguments. + */ + public Optional getOptionalGlobalMessageStrings() { + return Optional.ofNullable(globalMessageStrings); + } + + /** + * An array of reportingDescriptor objects relevant to the analysis performed by the tool component. + */ + public Optional> getOptionalRules() { + return Optional.ofNullable(rules); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/CheckstyleParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/CheckstyleParser.java index e50be8d57997..f9fe1b9d90ba 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/CheckstyleParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/CheckstyleParser.java @@ -40,9 +40,9 @@ public class CheckstyleParser implements ParserStrategy { private final XmlMapper xmlMapper = new XmlMapper(); @Override - public StaticCodeAnalysisReportDTO parse(String xmlContent) { + public StaticCodeAnalysisReportDTO parse(String reportContent) { try { - List files = xmlMapper.readValue(xmlContent, new com.fasterxml.jackson.core.type.TypeReference>() { + List files = xmlMapper.readValue(reportContent, new com.fasterxml.jackson.core.type.TypeReference>() { }); return createReportFromFiles(files); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDCPDParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDCPDParser.java index 11fbab145fd4..b5da7a65fdf0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDCPDParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDCPDParser.java @@ -43,9 +43,9 @@ class PMDCPDParser implements ParserStrategy { private final XmlMapper xmlMapper = new XmlMapper(); @Override - public StaticCodeAnalysisReportDTO parse(String xmlContent) { + public StaticCodeAnalysisReportDTO parse(String reportContent) { try { - PmdCpc duplication = xmlMapper.readValue(xmlContent, PmdCpc.class); + PmdCpc duplication = xmlMapper.readValue(reportContent, PmdCpc.class); return createReportFromDuplication(duplication); } catch (IOException e) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java index ee7ce091b37b..7c7951630dc3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/PMDParser.java @@ -45,9 +45,9 @@ class PMDParser implements ParserStrategy { private final XmlMapper xmlMapper = new XmlMapper(); @Override - public StaticCodeAnalysisReportDTO parse(String xmlContent) { + public StaticCodeAnalysisReportDTO parse(String reportContent) { try { - PMDReport pmdReport = xmlMapper.readValue(xmlContent, PMDReport.class); + PMDReport pmdReport = xmlMapper.readValue(reportContent, PMDReport.class); return createReportFromPMDReport(pmdReport); } catch (IOException e) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/ParserStrategy.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/ParserStrategy.java index ea7e8e356179..cee6ca946062 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/ParserStrategy.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/ParserStrategy.java @@ -12,10 +12,10 @@ static String transformToUnixPath(String path) { } /** - * Parse a static code analysis report from an XML string into a common Java representation. + * Parse a static code analysis report from a serialized string into a common Java representation. * - * @param xmlContent The XML content as a String + * @param reportContent The serialized content as a String * @return Report object containing the parsed report information */ - StaticCodeAnalysisReportDTO parse(String xmlContent); + StaticCodeAnalysisReportDTO parse(String reportContent); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/SpotbugsParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/SpotbugsParser.java index f4db7cbb351b..51e5e3f2cee5 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/SpotbugsParser.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/SpotbugsParser.java @@ -49,9 +49,9 @@ class SpotbugsParser implements ParserStrategy { private final XmlMapper xmlMapper = new XmlMapper(); @Override - public StaticCodeAnalysisReportDTO parse(String xmlContent) { + public StaticCodeAnalysisReportDTO parse(String reportContent) { try { - BugCollection bugCollection = xmlMapper.readValue(xmlContent, BugCollection.class); + BugCollection bugCollection = xmlMapper.readValue(reportContent, BugCollection.class); return createReportFromBugCollection(bugCollection); } catch (IOException e) { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/IdCategorizer.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/IdCategorizer.java new file mode 100644 index 000000000000..646780fee2f5 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/IdCategorizer.java @@ -0,0 +1,11 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif; + +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor; + +class IdCategorizer implements RuleCategorizer { + + @Override + public String categorizeRule(ReportingDescriptor rule) { + return rule.id(); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/RuleCategorizer.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/RuleCategorizer.java new file mode 100644 index 000000000000..46898ec5d9c4 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/RuleCategorizer.java @@ -0,0 +1,14 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif; + +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor; + +public interface RuleCategorizer { + + /** + * Categorizes a SARIF rule using a tool specific strategy. + * + * @param rule The reporting descriptor containing the rule details + * @return The identifier of the resulting category + */ + String categorizeRule(ReportingDescriptor rule); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParser.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParser.java new file mode 100644 index 000000000000..46905fa6ed01 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParser.java @@ -0,0 +1,185 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisTool; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ArtifactLocation; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.GlobalMessageStrings; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Location; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.MessageStrings; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.PhysicalLocation; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Region; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptorReference; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Result; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.Run; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.SarifLog; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ToolComponent; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.ParserStrategy; + +/** + * Implements parts of the SARIF OASIS standard version 2.1.0. + * + * @see SARIF specification + */ +public class SarifParser implements ParserStrategy { + + private static final Logger log = LoggerFactory.getLogger(SarifParser.class); + + private static class SarifFormatException extends RuntimeException { + + private SarifFormatException(String message) { + super(message); + } + } + + private static class InformationMissingException extends RuntimeException { + + private InformationMissingException(String message) { + super(message); + } + } + + private record FileLocation(String path, int startLine, int endLine, int startColumn, int endColumn) { + } + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final StaticCodeAnalysisTool tool; + + private final RuleCategorizer ruleCategorizer; + + public SarifParser(StaticCodeAnalysisTool tool, RuleCategorizer ruleCategorizer) { + this.tool = tool; + this.ruleCategorizer = ruleCategorizer; + } + + @Override + public StaticCodeAnalysisReportDTO parse(String reportContent) { + SarifLog sarifLog; + try { + sarifLog = objectMapper.readValue(reportContent, SarifLog.class); + } + catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + + Run run = sarifLog.runs().getFirst(); + ToolComponent driver = run.tool().driver(); + + List rules = driver.getOptionalRules().orElse(List.of()); + + // Rule ids are not guaranteed to be unique. Use the first occurring for rule lookup. + Map ruleOfId = rules.stream().collect(Collectors.toMap(ReportingDescriptor::id, Function.identity(), (first, next) -> first)); + + List results = run.getOptionalResults().orElse(List.of()); + List issues = results.stream().map(result -> tryProcessResult(result, driver, ruleOfId)).filter(Objects::nonNull).toList(); + + return new StaticCodeAnalysisReportDTO(tool, issues); + } + + private StaticCodeAnalysisIssue tryProcessResult(Result result, ToolComponent driver, Map ruleOfId) { + try { + return processResult(result, driver, ruleOfId); + } + catch (SarifFormatException | NullPointerException e) { + log.error("The result is malformed", e); + return null; + } + catch (InformationMissingException e) { + log.warn("The result does not contain required information", e); + return null; + } + } + + private StaticCodeAnalysisIssue processResult(Result result, ToolComponent driver, Map ruleOfId) throws SarifFormatException { + FileLocation fileLocation = result.getOptionalLocations().flatMap(locations -> locations.stream().findFirst()).flatMap(Location::getOptionalPhysicalLocation) + .map(this::extractLocation).orElseThrow(() -> new InformationMissingException("Location needed")); + + String ruleId = getRuleId(result); + + Optional ruleIndex = getRuleIndex(result); + + Optional ruleByIndex = driver.getOptionalRules().flatMap(rules -> ruleIndex.map(rules::get)); + Optional rule = ruleByIndex.or(() -> lookupRuleById(ruleId, ruleOfId)); + + // Fallback to the rule identifier for the category + String category = rule.map(ruleCategorizer::categorizeRule).orElse(ruleId); + + Result.Level level = result.getOptionalLevel().orElse(Result.Level.WARNING); + + String message = findMessage(result, driver, rule); + + return new StaticCodeAnalysisIssue(fileLocation.path(), fileLocation.startLine(), fileLocation.endLine(), fileLocation.startColumn(), fileLocation.endColumn(), ruleId, + category, message, level.toString(), null); + } + + private FileLocation extractLocation(PhysicalLocation location) { + URI uri = URI + .create(location.getOptionalArtifactLocation().flatMap(ArtifactLocation::getOptionalUri).orElseThrow(() -> new InformationMissingException("File path needed"))); + + Region region = location.getOptionalRegion().orElseThrow(() -> new SarifFormatException("Region must be present")); + + int startLine = region.getOptionalStartLine().orElseThrow(() -> new InformationMissingException("Text region needed")); + int startColumn = region.getOptionalStartColumn().orElse(1); + int endLine = region.getOptionalEndLine().orElse(startLine); + int endColumn = region.getOptionalEndColumn().orElse(startColumn + 1); + + return new FileLocation(uri.getPath(), startLine, endLine, startColumn, endColumn); + } + + private static String getRuleId(Result result) throws SarifFormatException { + return result.getOptionalRuleId().orElseGet(() -> result.getOptionalRule().flatMap(ReportingDescriptorReference::getOptionalId) + .orElseThrow(() -> new SarifFormatException("Either ruleId or rule.id must be present"))); + } + + private static Optional getRuleIndex(Result result) { + // ruleIndex can use -1 to indicate a missing value + Optional ruleIndexOrMinusOne = result.getOptionalRuleIndex().or(() -> result.getOptionalRule().flatMap(ReportingDescriptorReference::getOptionalIndex)); + return ruleIndexOrMinusOne.flatMap(index -> index != -1 ? Optional.of(index) : Optional.empty()); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static String findMessage(Result result, ToolComponent driver, Optional rule) throws SarifFormatException { + return result.message().getOptionalText().orElseGet(() -> lookupMessageById(result, driver, rule)); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static String lookupMessageById(Result result, ToolComponent driver, Optional rule) throws SarifFormatException { + String messageId = result.message().getOptionalId().orElseThrow(() -> new SarifFormatException("Either text or id must be present")); + + var ruleMessageString = rule.flatMap(ReportingDescriptor::getOptionalMessageStrings).map(MessageStrings::additionalProperties).map(strings -> strings.get(messageId)); + var globalMessageString = driver.getOptionalGlobalMessageStrings().map(GlobalMessageStrings::additionalProperties).map(strings -> strings.get(messageId)); + + var messageString = ruleMessageString.or(() -> globalMessageString).orElseThrow(() -> new SarifFormatException("Message lookup failed")); + return messageString.text(); + } + + private static Optional lookupRuleById(String ruleId, Map ruleOfId) { + return Optional.ofNullable(ruleOfId.get(ruleId)).or(() -> getBaseRuleId(ruleId).map(ruleOfId::get)); + } + + private static Optional getBaseRuleId(String ruleId) { + int hierarchySeperatorIndex = ruleId.lastIndexOf('/'); + if (hierarchySeperatorIndex == -1) { + return Optional.empty(); + } + String baseRuleId = ruleId.substring(0, hierarchySeperatorIndex); + return Optional.of(baseRuleId); + } + +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/utils/ReportUtils.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/utils/ReportUtils.java deleted file mode 100644 index 3b38d2597a4f..000000000000 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/utils/ReportUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.tum.cit.aet.artemis.programming.service.localci.scaparser.utils; - -import java.util.List; - -import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisTool; -import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; -import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; - -public final class ReportUtils { - - private ReportUtils() { - } - - /** - * Creates a report which states that the specified file is too large - * to be parsed by the parser. - * - * @param filename name of the parsed file - * @return report with the issue about the filesize - */ - public static StaticCodeAnalysisReportDTO createFileTooLargeReport(String filename) { - StaticCodeAnalysisTool tool = StaticCodeAnalysisTool.getToolByFilePattern(filename).orElse(null); - List issues = List.of(new StaticCodeAnalysisIssue(filename, 1, 1, 0, 0, // Assuming there are no column details - "TooManyIssues", "miscellaneous", String.format("There are too many issues found in the %s tool.", tool), null, // No priority for this issue - null // No penalty for this issue - )); - - return new StaticCodeAnalysisReportDTO(tool, issues); - } - - /** - * Creates a report wrapping an exception; Used to inform the client about any exception during parsing - * - * @param filename name of the parsed file - * @param exception exception to wrap - * @return a report for the file with an issue wrapping the exception - */ - public static StaticCodeAnalysisReportDTO createErrorReport(String filename, Exception exception) { - StaticCodeAnalysisTool tool = StaticCodeAnalysisTool.getToolByFilePattern(filename).orElse(null); - List issues = List.of(new StaticCodeAnalysisIssue(filename, 1, 1, 0, 0, // Assuming there are no column details - "ExceptionDuringParsing", "miscellaneous", - String.format("An exception occurred during parsing the report for %s. Exception: %s", tool != null ? tool : "file " + filename, exception), - // No priority and no penalty for this issue - null, null)); - - return new StaticCodeAnalysisReportDTO(tool, issues); - } -} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java index 2e8886ec0fbc..946900357324 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/StaticCodeAnalysisParserUnitTest.java @@ -2,21 +2,20 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; -import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.stream.Collectors; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; import de.tum.cit.aet.artemis.programming.service.localci.scaparser.ReportParser; -import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.ParserException; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.exception.UnsupportedToolException; /** * Tests each parser with an example file @@ -27,92 +26,61 @@ class StaticCodeAnalysisParserUnitTest { private static final Path REPORTS_FOLDER_PATH = Paths.get("src", "test", "resources", "test-data", "static-code-analysis", "reports"); + private final ObjectMapper mapper = new ObjectMapper(); + + private void testParserWithFile(String toolGeneratedReportFileName, String expectedJSONReportFileName) throws IOException { + testParserWithFileNamed(toolGeneratedReportFileName, toolGeneratedReportFileName, expectedJSONReportFileName); + } + /** * Compares the parsed JSON report with the expected JSON report * * @param toolGeneratedReportFileName The name of the file contains the report as generated by the different tools * @param expectedJSONReportFileName The name of the file that contains the parsed report - * @throws ParserException If an exception occurs that is not already handled by the parser itself, e.g. caused by the json-parsing */ - private void testParserWithFile(String toolGeneratedReportFileName, String expectedJSONReportFileName) throws ParserException, IOException { - File toolReport = REPORTS_FOLDER_PATH.resolve(toolGeneratedReportFileName).toFile(); + private void testParserWithFileNamed(String toolGeneratedReportFileName, String fileName, String expectedJSONReportFileName) throws IOException { + Path actualReportPath = REPORTS_FOLDER_PATH.resolve(toolGeneratedReportFileName); + File expectedJSONReportFile = EXPECTED_FOLDER_PATH.resolve(expectedJSONReportFileName).toFile(); - ReportParser parser = new ReportParser(); - String actual = parser.transformToJSONReport(toolReport); - - try (BufferedReader reader = Files.newBufferedReader(EXPECTED_FOLDER_PATH.resolve(expectedJSONReportFileName))) { - String expected = reader.lines().collect(Collectors.joining(System.lineSeparator())); - assertThat(actual).isEqualTo(expected); - } + String actualReportContent = Files.readString(actualReportPath); + testParserWithContent(actualReportContent, fileName, expectedJSONReportFile); } - private void testParserWithNullValue() throws ParserException { - ReportParser parser = new ReportParser(); - parser.transformToJSONReport(null); + private void testParserWithContent(String actualReportContent, String actualReportFilename, File expectedJSONReportFile) throws IOException { + StaticCodeAnalysisReportDTO actualReport = ReportParser.getReport(actualReportContent, actualReportFilename); + + StaticCodeAnalysisReportDTO expectedReport = mapper.readValue(expectedJSONReportFile, StaticCodeAnalysisReportDTO.class); + + assertThat(actualReport).isEqualTo(expectedReport); } @Test void testCheckstyleParser() throws IOException { - try { - testParserWithFile("checkstyle-result.xml", "checkstyle.txt"); - } - catch (ParserException e) { - fail("Checkstyle parser failed with exception: " + e.getMessage()); - } + testParserWithFile("checkstyle-result.xml", "checkstyle.txt"); } @Test void testPMDCPDParser() throws IOException { - try { - testParserWithFile("cpd.xml", "pmd_cpd.txt"); - } - catch (ParserException e) { - fail("PMD-CPD parser failed with exception: " + e.getMessage()); - } + testParserWithFile("cpd.xml", "pmd_cpd.txt"); } @Test void testPMDParser() throws IOException { - try { - testParserWithFile("pmd.xml", "pmd.txt"); - } - catch (ParserException e) { - fail("PMD parser failed with exception: " + e.getMessage()); - } + testParserWithFile("pmd.xml", "pmd.txt"); } @Test void testSpotbugsParser() throws IOException { - try { - testParserWithFile("spotbugsXml.xml", "spotbugs.txt"); - } - catch (ParserException e) { - fail("Spotbugs parser failed with exception: " + e.getMessage()); - } + testParserWithFile("spotbugsXml.xml", "spotbugs.txt"); } @Test - void testParseInvalidFilename() { - assertThatCode(() -> testParserWithFile("cpd_invalid.txt", "invalid_filename.txt")).isInstanceOf(ParserException.class); - } - - @Test - void testParseInvalidXML() throws Exception { - testParserWithFile("invalid_xml.xml", "invalid_xml.txt"); + void testParseInvalidXML() throws IOException { + assertThatCode(() -> testParserWithFileNamed("invalid_xml.xml", "pmd.xml", "invalid_xml.txt")).isInstanceOf(RuntimeException.class); } @Test void testInvalidName() throws IOException { - try { - testParserWithFile("invalid_name.xml", "invalid_name.txt"); - } - catch (ParserException e) { - fail("Parser failed with exception: " + e.getMessage()); - } - } - - @Test - void testThrowsParserException() { - assertThatExceptionOfType(ParserException.class).isThrownBy(this::testParserWithNullValue); + assertThatCode(() -> testParserWithFile("invalid_name.xml", "invalid_name.txt")).isInstanceOf(UnsupportedToolException.class); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java new file mode 100644 index 000000000000..a84df20165d7 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/service/localci/scaparser/strategy/sarif/SarifParserTest.java @@ -0,0 +1,476 @@ +package de.tum.cit.aet.artemis.programming.service.localci.scaparser.strategy.sarif; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import de.tum.cit.aet.artemis.programming.domain.StaticCodeAnalysisTool; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisIssue; +import de.tum.cit.aet.artemis.programming.dto.StaticCodeAnalysisReportDTO; +import de.tum.cit.aet.artemis.programming.service.localci.scaparser.format.sarif.ReportingDescriptor; + +class SarifParserTest { + + static class FullDescriptionCategorizer implements RuleCategorizer { + + @Override + public String categorizeRule(ReportingDescriptor rule) { + return rule.getOptionalFullDescription().orElseThrow().text(); + } + } + + @Test + void testEmpty() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [] + } + }, + "results": [] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).isEmpty(); + } + + @Test + void testMetadata() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "CATEGORY" + }, + "id": "RULE_ID" + } + ] + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 10, + "startColumn": 20, + "endLine": 30, + "endColumn": 40 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "RULE_ID", + "ruleIndex": 0 + } + ] + } + ] + } + """; + StaticCodeAnalysisIssue expected = new StaticCodeAnalysisIssue("/path/to/file.txt", 10, 30, 20, 40, "RULE_ID", "CATEGORY", "MESSAGE", "error", null); + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().isEqualTo(expected); + } + + @Test + void testMessageLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION" + }, + "id": "A001", + "messageStrings": { + "MESSAGE_ID_A": { + "text": "RULE_MESSAGE_CONTENT_A" + } + } + } + ], + "globalMessageStrings": { + "MESSAGE_ID_A": { + "text": "GLOBAL_MESSAGE_CONTENT_A" + }, + "MESSAGE_ID_B": { + "text": "GLOBAL_MESSAGE_CONTENT_B" + } + } + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "MESSAGE_ID_A" + }, + "ruleId": "A001", + "ruleIndex": 0 + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "MESSAGE_ID_B" + }, + "ruleId": "B001" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).anyMatch(issue -> issue.rule().equals("A001") && issue.message().equals("RULE_MESSAGE_CONTENT_A")) + .anyMatch(issue -> issue.rule().equals("B001") && issue.message().equals("GLOBAL_MESSAGE_CONTENT_B")); + } + + @Test + void testHierarchicalRuleIdLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION" + }, + "id": "A123" + } + ] + } + }, + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "A123/subrule" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.category().equals("FULL_DESCRIPTION")); + } + + @Test + void testRuleIndexLookup() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": { + "rules": [ + { + "fullDescription": { + "text": "FULL_DESCRIPTION_A" + }, + "id": "RULE_ID" + }, + { + "fullDescription": { + "text": "FULL_DESCRIPTION_B" + }, + "id": "RULE_ID" + } + ] + } + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "MESSAGE" + }, + "ruleId": "RULE_ID", + "ruleIndex": 1 + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new FullDescriptionCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.category().equals("FULL_DESCRIPTION_B")); + } + + @Test + void testInvalidJSON() { + String report = """ + { + "runs": [ + { + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + assertThatThrownBy(() -> parser.parse(report)).hasCauseInstanceOf(JsonProcessingException.class); + } + + @Test + void testFilterMalformedSarif() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": {} + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "VALID" + }, + "ruleId": "A001" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + } + } + } + ], + "message": { + "text": "REGION MISSING" + }, + "ruleId": "A002" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "NO_RULE_ID" + } + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "id": "INVALID_MESSAGE_ID" + }, + "ruleId": "A004" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.rule().equals("A001")); + } + + @Test + void testFilterInformationMissing() { + String report = """ + { + "runs": [ + { + "tool": { + "driver": {} + }, + "results": [ + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "VALID" + }, + "ruleId": "A001" + }, + { + "message": { + "text": "LOCATION MISSING" + }, + "ruleId": "A002" + }, + { + "locations": [ + { + "physicalLocation": { + "region": { + "startLine": 1 + } + } + } + ], + "message": { + "text": "PATH MISSING" + }, + "ruleId": "A003" + }, + { + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///path/to/file.txt" + }, + "region": { + "byteOffset": 0, + "byteLength": 10 + } + } + } + ], + "message": { + "text": "NOT A TEXT REGION" + }, + "ruleId": "A004" + } + ] + } + ] + } + """; + + SarifParser parser = new SarifParser(StaticCodeAnalysisTool.OTHER, new IdCategorizer()); + StaticCodeAnalysisReportDTO parsedReport = parser.parse(report); + + assertThat(parsedReport.issues()).singleElement().matches(issue -> issue.rule().equals("A001")); + } +} diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java index e4dca9d8be57..bf4c207df1e6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/util/ProgrammingExerciseFactory.java @@ -341,6 +341,7 @@ private static StaticCodeAnalysisIssue generateStaticCodeAnalysisIssue(StaticCod case PMD_CPD -> "Copy/Paste Detection"; case SWIFTLINT -> "swiftLint"; // TODO: rene: set better value after categories are better defined case GCC -> "Memory"; + case OTHER -> "Other"; }; return new StaticCodeAnalysisIssue(Constants.STUDENT_WORKING_DIRECTORY + "/www/packagename/Class1.java", // filePath diff --git a/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt b/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt deleted file mode 100644 index 135463d9785e..000000000000 --- a/src/test/resources/test-data/static-code-analysis/reports/cpd_invalid.txt +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - createRandomDatesList() throws ParseException { - int listLength = randomIntegerWithin(RANDOM_FLOOR, RANDOM_CEILING); - List list = new ArrayList<>(); - - SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy"); - Date lowestDate = dateFormat.parse("08.11.2016"); - Date highestDate = dateFormat.parse("03.11.2020"); - - for (int i = 0; i < listLength; i++) { - Date randomDate = randomDateWithin(lowestDate, highestDate); - list.add(randomDate); - } - return list; - } - - private static List createRandomDatesList2() throws ParseException {]]> - - - - - - -