diff --git a/build.gradle b/build.gradle index f8c5e28..ef40cfe 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ repositories { defaultTasks('installDist') -version = '0.0.2' +version = '0.0.3' jar.archiveName = "${jar.baseName}.${jar.extension}" distZip.archiveName = "${jar.baseName}.zip" @@ -65,6 +65,8 @@ dependencies { implementation 'org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r' + implementation 'com.contrastsecurity:java-sarif:2.0' + implementation 'org.apache.logging.log4j:log4j-core:2.17.1' implementation 'org.slf4j:slf4j-nop:2.0.0-alpha5' diff --git a/src/main/java/com/amazonaws/gurureviewercli/adapter/ResultsAdapter.java b/src/main/java/com/amazonaws/gurureviewercli/adapter/ResultsAdapter.java index e7c687a..7f3b2f0 100644 --- a/src/main/java/com/amazonaws/gurureviewercli/adapter/ResultsAdapter.java +++ b/src/main/java/com/amazonaws/gurureviewercli/adapter/ResultsAdapter.java @@ -3,12 +3,31 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; +import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; +import com.contrastsecurity.sarif.ArtifactLocation; +import com.contrastsecurity.sarif.Location; +import com.contrastsecurity.sarif.Message; +import com.contrastsecurity.sarif.MultiformatMessageString; +import com.contrastsecurity.sarif.PhysicalLocation; +import com.contrastsecurity.sarif.PropertyBag; +import com.contrastsecurity.sarif.Region; +import com.contrastsecurity.sarif.ReportingConfiguration; +import com.contrastsecurity.sarif.ReportingDescriptor; +import com.contrastsecurity.sarif.Result; +import com.contrastsecurity.sarif.Run; +import com.contrastsecurity.sarif.SarifSchema210; +import com.contrastsecurity.sarif.Tool; +import com.contrastsecurity.sarif.ToolComponent; import lombok.val; import org.commonmark.node.Node; import org.commonmark.parser.Parser; @@ -30,6 +49,10 @@ public static void saveResults(final Path outputDir, val jsonFile = outputDir.resolve("recommendations.json"); JsonUtil.storeRecommendations(results, jsonFile); Log.info("Recommendations in Json format written to to:%n%s", jsonFile.normalize().toUri()); + val sarifFile = outputDir.resolve("recommendations.sarif.json"); + JsonUtil.writeSarif(createSarifReport(results), sarifFile); + Log.info("Recommendations in SARIF format written to to:%n%s", sarifFile.normalize().toUri()); + createHtmlReport(outputDir, scanMetaData, results); } @@ -119,6 +142,89 @@ private static void createHtmlReport(final Path outputDir, Log.info("Report with %d recommendations written to:%n%s", validFindings, htmlFile.normalize().toUri()); } + private static SarifSchema210 createSarifReport(final List recommendations) + throws IOException { + val docUrl = "https://docs.aws.amazon.com/codeguru/latest/reviewer-ug/how-codeguru-reviewer-works.html"; + + val rulesMap = createSarifRuleDescriptions(recommendations); + val driver = new ToolComponent().withName("CodeGuru Reviewer Scanner") + .withInformationUri(URI.create(docUrl)) + .withRules(new HashSet<>(rulesMap.values())); + + val results = recommendations.stream().map(ResultsAdapter::convertToSarif) + .collect(Collectors.toList()); + + val run = new Run().withTool(new Tool().withDriver(driver)).withResults(results); + + return new SarifSchema210() + .withVersion(SarifSchema210.Version._2_1_0) + .with$schema(URI.create("http://json.schemastore.org/sarif-2.1.0-rtm.4")) + .withRuns(Arrays.asList(run)); + + } + + private static Map createSarifRuleDescriptions( + final List recommendations) { + val rulesMap = new HashMap(); + for (val recommendation : recommendations) { + val metaData = recommendation.ruleMetadata(); + if (metaData != null && !rulesMap.containsKey(metaData.ruleId())) { + val properties = new PropertyBag().withTags(new HashSet<>(metaData.ruleTags())); + MultiformatMessageString foo; + val descriptor = new ReportingDescriptor() + .withName(metaData.ruleName()) + .withId(metaData.ruleId()) + .withShortDescription(new MultiformatMessageString().withText(metaData.ruleName())) + .withFullDescription(new MultiformatMessageString().withText(metaData.shortDescription())) + .withHelp(new MultiformatMessageString().withText(metaData.longDescription())) + .withProperties(properties); + if (recommendation.severityAsString() != null) { + val level = ReportingConfiguration.Level.fromValue(getSarifSeverity(recommendation)); + descriptor.setDefaultConfiguration(new ReportingConfiguration().withLevel(level)); + } + rulesMap.put(metaData.ruleId(), descriptor); + } + } + return rulesMap; + } + + private static Result convertToSarif(final RecommendationSummary recommendation) { + List locations = Arrays.asList(getSarifLocation(recommendation)); + return new Result().withRuleId(recommendation.ruleMetadata().ruleId()) + .withLevel(Result.Level.fromValue(getSarifSeverity(recommendation))) + .withMessage(new Message().withMarkdown(recommendation.description())) + .withLocations(locations); + } + + private static Location getSarifLocation(final RecommendationSummary recommendation) { + val loc = new PhysicalLocation() + .withArtifactLocation(new ArtifactLocation().withUri(recommendation.filePath())) + .withRegion(new Region().withStartLine(recommendation.startLine()) + .withEndLine(recommendation.endLine())); + return new Location() + .withPhysicalLocation(loc); + } + + private static String getSarifSeverity(RecommendationSummary recommendation) { + if (recommendation.severity() == null) { + return Result.Level.NONE.value(); // can happen for legacy rules + } + switch (recommendation.severity()) { + case INFO: + return Result.Level.NONE.value(); + case LOW: + return Result.Level.NONE.value(); + case MEDIUM: + return Result.Level.NONE.value(); + case HIGH: + return Result.Level.WARNING.value(); + case CRITICAL: + return Result.Level.ERROR.value(); + default: + return Result.Level.NONE.value(); + } + } + private static void sortByFileName(final List recommendations) { Collections.sort(recommendations, (o1, o2) -> { int pathComp = o1.filePath().compareTo(o2.filePath()); diff --git a/src/main/java/com/amazonaws/gurureviewercli/util/JsonUtil.java b/src/main/java/com/amazonaws/gurureviewercli/util/JsonUtil.java index 23327bb..78c036f 100644 --- a/src/main/java/com/amazonaws/gurureviewercli/util/JsonUtil.java +++ b/src/main/java/com/amazonaws/gurureviewercli/util/JsonUtil.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.stream.Collectors; +import com.contrastsecurity.sarif.SarifSchema210; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.PropertyAccessor; @@ -39,6 +40,11 @@ public static void storeRecommendations(@NonNull final List