Skip to content

Commit 7519bae

Browse files
authored
Support for BitBucket CodeInsights format
Adding bitbucket codeinsight export format
1 parent 8855a9b commit 7519bae

File tree

12 files changed

+298
-6
lines changed

12 files changed

+298
-6
lines changed

Diff for: .github/workflows/cicd-demo.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ jobs:
4040
- name: Download CodeGuru Reviewer CLI
4141
shell: bash
4242
env:
43-
VERSION: 0.1.0
44-
run: curl -OL "https://github.com/martinschaef/aws-codeguru-cli/releases/download/$VERSION/aws-codeguru-cli.zip"
43+
VERSION: 0.2.1
44+
run: curl -OL "https://github.com/aws/aws-codeguru-cli/releases/download/$VERSION/aws-codeguru-cli.zip"
4545
- run: unzip aws-codeguru-cli.zip
4646

4747
# Run CodeGuru Reviewer on the current project and use the --fail-on-recommendations option to fail

Diff for: .github/workflows/self-test-and-release.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
jq -r '.runs[0].results[] | {ruleId: .ruleId, firstFile: .locations[0].physicalLocation.artifactLocation.uri, firstLine: .locations[0].physicalLocation.region.startLine} | join(",")' \
6060
code-guru/recommendations.sarif.json | sort >> summary-of-results.csv
6161
cat summary-of-results.csv
62-
62+
6363
- name: Validate Findings ${{ matrix.os }}
6464
if: steps.iam-role.outcome == 'success'
6565
shell: bash
@@ -136,7 +136,7 @@ jobs:
136136
jq -r '.runs[0].results[] | {ruleId: .ruleId, firstFile: .locations[0].physicalLocation.artifactLocation.uri, firstLine: .locations[0].physicalLocation.region.startLine} | join(",")' \
137137
code-guru/recommendations.sarif.json | sort >> summary-of-results.csv
138138
cat summary-of-results.csv
139-
139+
140140
- name: Validate Findings ${{ matrix.os }}
141141
if: steps.iam-role.outcome == 'success'
142142
shell: bash

Diff for: build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repositories {
2323

2424
defaultTasks 'clean', 'check', 'installDist'
2525

26-
version = '0.1.0'
26+
version = '0.2.1'
2727
jar.archiveName = "${jar.baseName}.${jar.extension}"
2828
distZip.archiveName = "${jar.baseName}.zip"
2929

@@ -59,6 +59,8 @@ dependencies {
5959

6060
implementation 'com.google.code.findbugs:jsr305:3.0.2'
6161

62+
implementation("com.google.guava:guava:31.1-jre")
63+
6264
compileOnly 'org.projectlombok:lombok:1.18.22'
6365
annotationProcessor 'org.projectlombok:lombok:1.18.22'
6466

Diff for: lombok.config

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
lombok.anyConstructor.addConstructorProperties=true

Diff for: src/main/java/com/amazonaws/gurureviewercli/Main.java

+14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.amazonaws.gurureviewercli;
22

33

4+
import java.io.File;
45
import java.io.IOException;
56
import java.net.URI;
67
import java.nio.file.Path;
@@ -16,6 +17,7 @@
1617
import lombok.val;
1718
import org.beryx.textio.TextIO;
1819
import org.beryx.textio.system.SystemTextTerminal;
20+
import org.eclipse.jgit.util.FileUtils;
1921
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
2022
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
2123
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
@@ -36,6 +38,7 @@
3638
import com.amazonaws.gurureviewercli.model.GitMetaData;
3739
import com.amazonaws.gurureviewercli.model.ScanMetaData;
3840
import com.amazonaws.gurureviewercli.model.configfile.CustomConfiguration;
41+
import com.amazonaws.gurureviewercli.util.CodeInsightExport;
3942
import com.amazonaws.gurureviewercli.util.Log;
4043
import com.amazonaws.gurureviewercli.util.RecommendationPrinter;
4144
import com.amazonaws.gurureviewercli.util.RecommendationsFilter;
@@ -68,6 +71,10 @@ public class Main {
6871
required = false)
6972
private boolean failOnRecommendations;
7073

74+
@Parameter(names = {"--bitbucket-code-insights"},
75+
description = "Output directory for Bitbucket insights report and annotation files.",
76+
required = false)
77+
private String bitbucketCodeInsightsDirectory;
7178
@Parameter(names = {"--root-dir", "-r"},
7279
description = "The root directory of the project that should be analyzed.",
7380
required = true)
@@ -165,6 +172,13 @@ public static void main(String[] argv) {
165172
}
166173
ResultsAdapter.saveResults(outputPath, results, scanMetaData);
167174
Log.info("Analysis finished.");
175+
176+
if (main.bitbucketCodeInsightsDirectory != null) {
177+
val bitBucketDir = new File(main.bitbucketCodeInsightsDirectory).getCanonicalFile();
178+
FileUtils.mkdirs(bitBucketDir, true);
179+
CodeInsightExport.report(results, scanMetaData, bitBucketDir.toPath());
180+
}
181+
168182
if (main.failOnRecommendations && !results.isEmpty()) {
169183
RecommendationPrinter.print(results);
170184
Log.error("Exiting with code 5 because %d recommendations were found and --fail-on-recommendations"

Diff for: src/main/java/com/amazonaws/gurureviewercli/model/Recommendation.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public class Recommendation {
2424
private String severity;
2525

2626
@Data
27-
static final class RuleMetadata {
27+
public static final class RuleMetadata {
2828
private String ruleId;
2929
private String ruleName;
3030
private String shortDescription;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.amazonaws.gurureviewercli.model.bitbucket;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
import lombok.extern.log4j.Log4j2;
9+
10+
/**
11+
* Bitbucket CodeInsight annotation.
12+
* See https://developer.atlassian.com/cloud/bitbucket/rest/api-group-reports/#api-group-reports
13+
*
14+
* Example
15+
* {
16+
* "external_id": "CodeGuruReviewer-02-annotation002",
17+
* "title": "Bug report",
18+
* "annotation_type": "BUG",
19+
* "summary": "This line might introduce a bug.",
20+
* "severity": "MEDIUM",
21+
* "path": "my-service/src/main/java/com/myCompany/mysystem/logic/Helper.java",
22+
* "line": 13
23+
* }
24+
*/
25+
@Log4j2
26+
@Builder
27+
@Data
28+
@AllArgsConstructor
29+
@NoArgsConstructor
30+
public class CodeInsightsAnnotation {
31+
32+
private String title;
33+
34+
@JsonProperty("external_id")
35+
private String externalId;
36+
37+
@JsonProperty("annotation_type")
38+
private String annotationType;
39+
40+
private String path;
41+
42+
private long line;
43+
44+
private String summary;
45+
46+
private String severity;
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.amazonaws.gurureviewercli.model.bitbucket;
2+
3+
import java.util.List;
4+
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Builder;
8+
import lombok.Data;
9+
import lombok.NoArgsConstructor;
10+
import lombok.extern.log4j.Log4j2;
11+
12+
/**
13+
* Bitbucket CodeInsight report format.
14+
* See https://developer.atlassian.com/cloud/bitbucket/rest/api-group-reports/#api-group-reports
15+
* Example:
16+
* {
17+
* "title": "Amazon CodeGuru Reviewer Scan Report",
18+
* "details": "Some more text.",
19+
* "report_type": "SECURITY",
20+
* "reporter": "Amazon CodeGuru Reviewer",
21+
* "link": "http://www.CodeGuruReviewer.com/reports/001",
22+
* "result": "FAILED",
23+
* "data": [
24+
* {
25+
* "title": "Duration (seconds)",
26+
* "type": "DURATION",
27+
* "value": 14
28+
* },
29+
* {
30+
* "title": "Safe to merge?",
31+
* "type": "BOOLEAN",
32+
* "value": false
33+
* }
34+
* ]
35+
* }
36+
*/
37+
@Log4j2
38+
@Builder
39+
@Data
40+
@AllArgsConstructor
41+
@NoArgsConstructor
42+
public class CodeInsightsReport {
43+
44+
private String title;
45+
46+
private String details;
47+
48+
private String result;
49+
50+
private String link;
51+
52+
private List<CodeInsightsReportData> data;
53+
54+
@JsonProperty("reporter")
55+
private String reporter;
56+
57+
@JsonProperty("report_type")
58+
private final String reportType = "SECURITY";
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.amazonaws.gurureviewercli.model.bitbucket;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import lombok.extern.log4j.Log4j2;
8+
9+
/**
10+
* Bitbucket CodeInsight report data.
11+
* See https://developer.atlassian.com/cloud/bitbucket/rest/api-group-reports/#api-group-reports
12+
*/
13+
@Log4j2
14+
@Builder
15+
@Data
16+
@AllArgsConstructor
17+
@NoArgsConstructor
18+
public class CodeInsightsReportData {
19+
20+
private String title;
21+
22+
private String type;
23+
24+
private Object value;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.amazonaws.gurureviewercli.util;
2+
3+
import java.io.IOException;
4+
import java.nio.file.Path;
5+
import java.util.ArrayList;
6+
import java.util.Collection;
7+
import java.util.stream.Collectors;
8+
9+
import com.fasterxml.jackson.annotation.JsonInclude;
10+
import com.fasterxml.jackson.databind.DeserializationFeature;
11+
import com.fasterxml.jackson.databind.SerializationFeature;
12+
import com.fasterxml.jackson.databind.json.JsonMapper;
13+
import lombok.val;
14+
import software.amazon.awssdk.services.codegurureviewer.model.RecommendationSummary;
15+
import software.amazon.awssdk.services.codegurureviewer.model.Severity;
16+
17+
import com.amazonaws.gurureviewercli.model.ScanMetaData;
18+
import com.amazonaws.gurureviewercli.model.bitbucket.CodeInsightsAnnotation;
19+
import com.amazonaws.gurureviewercli.model.bitbucket.CodeInsightsReport;
20+
21+
/**
22+
* Export Report and Annotations file for BitBucket CodeInsights.
23+
*/
24+
public final class CodeInsightExport {
25+
private static final String REPORT_FILE_NAME = "report.json";
26+
private static final String ANNOTATIONS_FILE_NAME = "annotations.json";
27+
28+
private static final JsonMapper JSON_MAPPER =
29+
JsonMapper.builder()
30+
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
31+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
32+
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
33+
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
34+
.enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
35+
.build();
36+
37+
public static void report(final Collection<RecommendationSummary> recommendations,
38+
final ScanMetaData scanMetaData,
39+
final Path outputDir) throws IOException {
40+
val reportTitle = "CodeGuru Reviewer report";
41+
val url = String.format("https://console.aws.amazon.com/codeguru/reviewer?region=%s#/codereviews/details/%s",
42+
scanMetaData.getRegion(), scanMetaData.getCodeReviewArn());
43+
val report = CodeInsightsReport.builder()
44+
.title(reportTitle)
45+
.reporter("CodeGuru Reviewer CLI")
46+
.details(String.format("CodeGuru Reviewer reported %d recommendations",
47+
recommendations.size()))
48+
.result(recommendations.isEmpty() ? "PASSED" : "FAILED")
49+
.link(url)
50+
.data(new ArrayList<>())
51+
.build();
52+
53+
val annotations = recommendations.stream().map(r -> convert(r, reportTitle))
54+
.collect(Collectors.toList());
55+
56+
JSON_MAPPER.writeValue(outputDir.resolve(REPORT_FILE_NAME).toFile(), report);
57+
JSON_MAPPER.writeValue(outputDir.resolve(ANNOTATIONS_FILE_NAME).toFile(), annotations);
58+
}
59+
60+
private static CodeInsightsAnnotation convert(final RecommendationSummary recommendation,
61+
final String reportTitle) {
62+
String description = recommendation.recommendationCategoryAsString();
63+
if (recommendation.ruleMetadata() != null) {
64+
description = recommendation.ruleMetadata().shortDescription();
65+
}
66+
67+
return CodeInsightsAnnotation.builder()
68+
.title(reportTitle)
69+
.externalId(recommendation.recommendationId())
70+
.path(recommendation.filePath())
71+
.line(recommendation.startLine())
72+
.summary(description)
73+
.annotationType("Vulnerability".toUpperCase())
74+
.severity(convertSeverity(recommendation.severity()))
75+
.build();
76+
}
77+
78+
private static String convertSeverity(Severity guruSeverity) {
79+
if (guruSeverity != null) {
80+
return guruSeverity.toString().toUpperCase(); // Bitbucket uses the same severity levels as CodeGuru.
81+
}
82+
return "Unknown";
83+
}
84+
85+
}

Diff for: src/main/java/com/amazonaws/gurureviewercli/util/Log.java

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package com.amazonaws.gurureviewercli.util;
22

3+
import java.io.PrintWriter;
4+
import java.io.StringWriter;
5+
36
import org.beryx.textio.TextTerminal;
47
import org.beryx.textio.system.SystemTextTerminal;
58

@@ -50,6 +53,10 @@ public static void awsUrl(final String format, final Object... args) {
5053

5154
public static void error(final Throwable t) {
5255
terminal.println(TEXT_RED + t.getMessage() + TEXT_RESET);
56+
StringWriter sw = new StringWriter();
57+
PrintWriter pw = new PrintWriter(sw);
58+
t.printStackTrace(pw);
59+
terminal.println(sw.toString());
5360
}
5461

5562
private Log() {

0 commit comments

Comments
 (0)