From 415ca77591ac2cb9a2ce31ded8de0d3d1721754a Mon Sep 17 00:00:00 2001 From: Artur Bosch Date: Mon, 16 Nov 2020 17:38:02 +0100 Subject: [PATCH] Support sarif as a report type - #3045 (#3132) * Support sarif as a report type - #3045 * Integrate sarif feedback from @lgolding * Test the whole sarif report instead of some json paths * Provide only the short description; we do not have access to the long one programmatically * Use the plain rule id as sarif rule name; the rule set id is encoded in the sarif rule id * Remove need for casting by using a when expression --- .../detekt/test/EmptySetupContext.kt | 18 +++++ detekt-report-sarif/build.gradle.kts | 24 ++++++ .../detekt/report/sarif/RuleDescriptors.kt | 43 ++++++++++ .../io/github/detekt/report/sarif/SarifDsl.kt | 47 +++++++++++ .../detekt/report/sarif/SarifOutputReport.kt | 64 +++++++++++++++ ....gitlab.arturbosch.detekt.api.OutputReport | 1 + .../report/sarif/SarifOutputReportSpec.kt | 41 ++++++++++ ....github.detekt.tooling.api.VersionProvider | 1 + .../src/test/resources/expected.sarif.json | 81 +++++++++++++++++++ settings.gradle.kts | 1 + 10 files changed, 321 insertions(+) create mode 100644 detekt-api/src/testFixtures/kotlin/io/gitlab/arturbosch/detekt/test/EmptySetupContext.kt create mode 100644 detekt-report-sarif/build.gradle.kts create mode 100644 detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/RuleDescriptors.kt create mode 100644 detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifDsl.kt create mode 100644 detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifOutputReport.kt create mode 100644 detekt-report-sarif/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport create mode 100644 detekt-report-sarif/src/test/kotlin/io/github/detekt/report/sarif/SarifOutputReportSpec.kt create mode 100644 detekt-report-sarif/src/test/resources/META-INF/services/io.github.detekt.tooling.api.VersionProvider create mode 100644 detekt-report-sarif/src/test/resources/expected.sarif.json diff --git a/detekt-api/src/testFixtures/kotlin/io/gitlab/arturbosch/detekt/test/EmptySetupContext.kt b/detekt-api/src/testFixtures/kotlin/io/gitlab/arturbosch/detekt/test/EmptySetupContext.kt new file mode 100644 index 00000000000..1ef301c11db --- /dev/null +++ b/detekt-api/src/testFixtures/kotlin/io/gitlab/arturbosch/detekt/test/EmptySetupContext.kt @@ -0,0 +1,18 @@ +package io.gitlab.arturbosch.detekt.test + +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.SetupContext +import io.gitlab.arturbosch.detekt.api.UnstableApi +import java.net.URI + +@OptIn(UnstableApi::class) +class EmptySetupContext : SetupContext { + override val configUris: Collection = emptyList() + override val config: Config = Config.empty + override val outputChannel: Appendable = StringBuilder() + override val errorChannel: Appendable = StringBuilder() + override val properties: MutableMap = HashMap() + override fun register(key: String, value: Any) { + properties[key] = value + } +} diff --git a/detekt-report-sarif/build.gradle.kts b/detekt-report-sarif/build.gradle.kts new file mode 100644 index 00000000000..b6e11b9e888 --- /dev/null +++ b/detekt-report-sarif/build.gradle.kts @@ -0,0 +1,24 @@ +repositories { + mavenLocal() +} + +dependencies { + compileOnly(project(":detekt-api")) + compileOnly(project(":detekt-tooling")) + implementation("io.github.detekt.sarif4j:sarif4j:1.0.0") + testImplementation(project(":detekt-tooling")) + testImplementation(project(":detekt-test-utils")) + testImplementation(testFixtures(project(":detekt-api"))) +} + +tasks.withType().configureEach { + dependsOn(configurations.runtimeClasspath) + from({ + configurations.runtimeClasspath.get() + .asSequence() + .filterNot { "org.jetbrains" in it.toString() } + .filterNot { "org.intellij" in it.toString() } + .map { if (it.isDirectory) it else zipTree(it) } + .toList() + }) +} diff --git a/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/RuleDescriptors.kt b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/RuleDescriptors.kt new file mode 100644 index 00000000000..0187fb8360c --- /dev/null +++ b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/RuleDescriptors.kt @@ -0,0 +1,43 @@ +package io.github.detekt.report.sarif + +import io.github.detekt.sarif4j.MultiformatMessageString +import io.github.detekt.sarif4j.ReportingDescriptor +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.MultiRule +import io.gitlab.arturbosch.detekt.api.Rule +import io.gitlab.arturbosch.detekt.api.RuleSetId +import io.gitlab.arturbosch.detekt.api.RuleSetProvider +import java.net.URI +import java.util.ServiceLoader + +fun ruleDescriptors(config: Config): HashMap { + val sets = ServiceLoader.load(RuleSetProvider::class.java) + .map { it.instance(config.subConfig(it.ruleSetId)) } + val descriptors = HashMap() + for (ruleSet in sets) { + for (rule in ruleSet.rules) { + when (rule) { + is MultiRule -> { + descriptors.putAll(rule.toDescriptors(ruleSet.id).associateBy { it.name }) + } + is Rule -> { + val descriptor = rule.toDescriptor(ruleSet.id) + descriptors[descriptor.name] = descriptor + } + } + } + } + return descriptors +} + +fun descriptor(init: ReportingDescriptor.() -> Unit) = ReportingDescriptor().apply(init) + +fun MultiRule.toDescriptors(ruleSetId: RuleSetId): List = + this.rules.map { it.toDescriptor(ruleSetId) } + +fun Rule.toDescriptor(ruleSetId: RuleSetId): ReportingDescriptor = descriptor { + id = "detekt.$ruleSetId.$ruleId" + name = ruleId + shortDescription = MultiformatMessageString().apply { text = issue.description } + helpUri = URI.create("https://detekt.github.io/detekt/${ruleSetId.toLowerCase()}.html#${ruleId.toLowerCase()}") +} diff --git a/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifDsl.kt b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifDsl.kt new file mode 100644 index 00000000000..261ac45815b --- /dev/null +++ b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifDsl.kt @@ -0,0 +1,47 @@ +package io.github.detekt.report.sarif + +import io.github.detekt.sarif4j.Run +import io.github.detekt.sarif4j.SarifSchema210 +import io.github.detekt.sarif4j.Tool +import io.github.detekt.sarif4j.ToolComponent +import io.github.detekt.tooling.api.VersionProvider +import io.gitlab.arturbosch.detekt.api.Config +import java.net.URI + +const val SCHEMA_URL = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" + +fun sarif(init: SarifSchema210.() -> Unit): SarifSchema210 = SarifSchema210() + .`with$schema`(URI.create(SCHEMA_URL)) + .withVersion(SarifSchema210.Version._2_1_0) + .withRuns(ArrayList()) + .apply(init) + +typealias SarifIssue = io.github.detekt.sarif4j.Result + +fun result(init: SarifIssue.() -> Unit): SarifIssue = SarifIssue().withLocations(ArrayList()).apply(init) + +fun tool(init: Tool.() -> Unit): Tool = Tool().apply(init) + +fun component(init: ToolComponent.() -> Unit): ToolComponent = ToolComponent().apply(init) + +fun SarifSchema210.withDetektRun(config: Config, init: Run.() -> Unit) { + runs.add( + Run() + .withResults(ArrayList()) + .withTool(tool { + driver = component { + guid = "022ca8c2-f6a2-4c95-b107-bb72c43263f3" + name = "detekt" + fullName = name + organization = name + language = "en" + version = VersionProvider.load().current() + semanticVersion = version + downloadUri = URI.create("https://github.com/detekt/detekt/releases/download/v$version/detekt") + informationUri = URI.create("https://detekt.github.io/detekt") + rules = ruleDescriptors(config).values.toSet() + } + }) + .apply(init) + ) +} diff --git a/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifOutputReport.kt b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifOutputReport.kt new file mode 100644 index 00000000000..1737dabf5f9 --- /dev/null +++ b/detekt-report-sarif/src/main/kotlin/io/github/detekt/report/sarif/SarifOutputReport.kt @@ -0,0 +1,64 @@ +package io.github.detekt.report.sarif + +import io.github.detekt.sarif4j.ArtifactLocation +import io.github.detekt.sarif4j.JacksonSarifWriter +import io.github.detekt.sarif4j.Location +import io.github.detekt.sarif4j.Message +import io.github.detekt.sarif4j.PhysicalLocation +import io.github.detekt.sarif4j.Region +import io.github.detekt.sarif4j.Result +import io.gitlab.arturbosch.detekt.api.Config +import io.gitlab.arturbosch.detekt.api.Detektion +import io.gitlab.arturbosch.detekt.api.Finding +import io.gitlab.arturbosch.detekt.api.OutputReport +import io.gitlab.arturbosch.detekt.api.RuleSetId +import io.gitlab.arturbosch.detekt.api.SetupContext +import io.gitlab.arturbosch.detekt.api.SingleAssign +import io.gitlab.arturbosch.detekt.api.UnstableApi +import java.net.URI + +class SarifOutputReport : OutputReport() { + + override val ending: String = "sarif" + override val id: String = "sarif" + override val name = "SARIF: a standard format for the output of static analysis tools" + + private var config: Config by SingleAssign() + + @OptIn(UnstableApi::class) + override fun init(context: SetupContext) { + this.config = context.config + } + + override fun render(detektion: Detektion): String { + val report = sarif { + withDetektRun(config) { + for ((ruleSetId, issues) in detektion.findings) { + for (issue in issues) { + results.add(issue.toIssue(ruleSetId)) + } + } + } + } + return JacksonSarifWriter().toJson(report) + } +} + +fun Finding.toIssue(ruleSetId: RuleSetId): SarifIssue = result { + ruleId = "detekt.$ruleSetId.$id" + level = Result.Level.WARNING + for (location in listOf(location) + references.map { it.location }) { + locations.add(Location().apply { + physicalLocation = PhysicalLocation().apply { + region = Region().apply { + startLine = location.source.line + startColumn = location.source.column + } + artifactLocation = ArtifactLocation().apply { + uri = URI.create(location.file).toString() + } + } + }) + } + message = Message().apply { text = messageOrDescription() } +} diff --git a/detekt-report-sarif/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport b/detekt-report-sarif/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport new file mode 100644 index 00000000000..f448fcc1d05 --- /dev/null +++ b/detekt-report-sarif/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.OutputReport @@ -0,0 +1 @@ +io.github.detekt.report.sarif.SarifOutputReport diff --git a/detekt-report-sarif/src/test/kotlin/io/github/detekt/report/sarif/SarifOutputReportSpec.kt b/detekt-report-sarif/src/test/kotlin/io/github/detekt/report/sarif/SarifOutputReportSpec.kt new file mode 100644 index 00000000000..12f6abb55c0 --- /dev/null +++ b/detekt-report-sarif/src/test/kotlin/io/github/detekt/report/sarif/SarifOutputReportSpec.kt @@ -0,0 +1,41 @@ +package io.github.detekt.report.sarif + +import io.github.detekt.test.utils.readResourceContent +import io.github.detekt.tooling.api.VersionProvider +import io.gitlab.arturbosch.detekt.test.EmptySetupContext +import io.gitlab.arturbosch.detekt.test.TestDetektion +import io.gitlab.arturbosch.detekt.test.createFinding +import org.assertj.core.api.Assertions.assertThat +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +class SarifOutputReportSpec : Spek({ + + describe("sarif output report") { + + val expectedReport by memoized { + readResourceContent("expected.sarif.json").stripWhitespace() + } + + it("renders multiple issues") { + val result = TestDetektion( + createFinding(ruleName = "TestSmellA"), + createFinding(ruleName = "TestSmellB"), + createFinding(ruleName = "TestSmellC") + ) + + val report = SarifOutputReport().apply { init(EmptySetupContext()) } + .render(result) + .stripWhitespace() + + assertThat(report).isEqualTo(expectedReport) + } + } +}) + +internal fun String.stripWhitespace() = replace(Regex("\\s"), "") + +internal class TestVersionProvider : VersionProvider { + + override fun current(): String = "1.0.0" +} diff --git a/detekt-report-sarif/src/test/resources/META-INF/services/io.github.detekt.tooling.api.VersionProvider b/detekt-report-sarif/src/test/resources/META-INF/services/io.github.detekt.tooling.api.VersionProvider new file mode 100644 index 00000000000..3d3ba0d70d8 --- /dev/null +++ b/detekt-report-sarif/src/test/resources/META-INF/services/io.github.detekt.tooling.api.VersionProvider @@ -0,0 +1 @@ +io.github.detekt.report.sarif.TestVersionProvider diff --git a/detekt-report-sarif/src/test/resources/expected.sarif.json b/detekt-report-sarif/src/test/resources/expected.sarif.json new file mode 100644 index 00000000000..e8e980b404e --- /dev/null +++ b/detekt-report-sarif/src/test/resources/expected.sarif.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "guid": "022ca8c2-f6a2-4c95-b107-bb72c43263f3", + "name": "detekt", + "organization": "detekt", + "fullName": "detekt", + "version": "1.0.0", + "semanticVersion": "1.0.0", + "downloadUri": "https://github.com/detekt/detekt/releases/download/v1.0.0/detekt", + "informationUri": "https://detekt.github.io/detekt", + "rules": [], + "language": "en" + } + }, + "results": [ + { + "ruleId": "detekt.TestSmellA.TestSmellA", + "message": { + "text": "TestMessage" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TestFile.kt" + }, + "region": { + "startLine": 1, + "startColumn": 1 + } + } + } + ] + }, + { + "ruleId": "detekt.TestSmellB.TestSmellB", + "message": { + "text": "TestMessage" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TestFile.kt" + }, + "region": { + "startLine": 1, + "startColumn": 1 + } + } + } + ] + }, + { + "ruleId": "detekt.TestSmellC.TestSmellC", + "message": { + "text": "TestMessage" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TestFile.kt" + }, + "region": { + "startLine": 1, + "startColumn": 1 + } + } + } + ] + } + ] + } + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts index ad5fa464355..fc1d3bd484d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ include( "detekt-parser", "detekt-psi-utils", "detekt-report-html", + "detekt-report-sarif", "detekt-report-txt", "detekt-report-xml", "detekt-rules",