Skip to content

Commit

Permalink
Development: Add SARIF parser (#9609)
Browse files Browse the repository at this point in the history
  • Loading branch information
magaupp authored and AjayvirS committed Dec 3, 2024
1 parent a0bdbee commit f029b81
Show file tree
Hide file tree
Showing 33 changed files with 1,383 additions and 286 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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"));
}

/**
Expand All @@ -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<StaticCodeAnalysisReportDTO> staticCodeAnalysisReports, String buildJobId) {
private void processStaticCodeAnalysisReportFile(String fileName, String reportContent, List<StaticCodeAnalysisReportDTO> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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<ProgrammingLanguage, List<StaticCodeAnalysisTool>> 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;
}

/**
Expand All @@ -41,13 +43,7 @@ public String getFilePattern() {
* @return List of static code analysis
*/
public static List<StaticCodeAnalysisTool> getToolsForProgrammingLanguage(ProgrammingLanguage language) {
List<StaticCodeAnalysisTool> 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());
}

/**
Expand All @@ -58,7 +54,7 @@ public static List<StaticCodeAnalysisTool> getToolsForProgrammingLanguage(Progra
*/
public static Optional<StaticCodeAnalysisTool> getToolByFilePattern(String fileName) {
for (StaticCodeAnalysisTool tool : StaticCodeAnalysisTool.values()) {
if (Objects.equals(fileName, tool.filePattern)) {
if (Objects.equals(fileName, tool.fileName)) {
return Optional.of(tool);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<String> getOptionalUri() {
return Optional.ofNullable(uri);
}

}
Original file line number Diff line number Diff line change
@@ -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<String, MultiformatMessageString> additionalProperties) {

}
Original file line number Diff line number Diff line change
@@ -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<PhysicalLocation> getOptionalPhysicalLocation() {
return Optional.ofNullable(physicalLocation);
}

}
Original file line number Diff line number Diff line change
@@ -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<String> getOptionalText() {
return Optional.ofNullable(text);
}

/**
* The identifier for this message.
*/
public Optional<String> getOptionalId() {
return Optional.ofNullable(id);
}

}
Original file line number Diff line number Diff line change
@@ -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<String, MultiformatMessageString> additionalProperties) {

}
Original file line number Diff line number Diff line change
@@ -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) {
}
Original file line number Diff line number Diff line change
@@ -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<ArtifactLocation> getOptionalArtifactLocation() {
return Optional.ofNullable(artifactLocation);
}

/**
* A region within an artifact where a result was detected.
*/
public Optional<Region> getOptionalRegion() {
return Optional.ofNullable(region);
}

}
Loading

0 comments on commit f029b81

Please sign in to comment.