Skip to content
This repository was archived by the owner on Oct 14, 2024. It is now read-only.

Commit 0729231

Browse files
authored
Misc sarif updates (#80)
* Extract levelOption() util * Add level option to merging * Extract RESULT_SORT_COMPARATOR * Add identity and shallow hash * Mark merged lint baselines as suppressed * Spotless and API dump * Extract util * Reuse SarifSerializer * Extract merger * Spotless * Implement ApplyBaselinesToSarifs * Detekt
1 parent ce42036 commit 0729231

File tree

5 files changed

+397
-87
lines changed

5 files changed

+397
-87
lines changed

api/kotlin-cli-util.api

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,20 @@ public final class slack/cli/lint/LintBaselineMergerCli : com/github/ajalt/clikt
8080
public fun run ()V
8181
}
8282

83-
public synthetic class slack/cli/lint/LintBaselineMergerCli$EntriesMappings {
84-
public static final synthetic field entries$0 Lkotlin/enums/EnumEntries;
83+
public final class slack/cli/lint/LintBaselineMergerCli$Factory : slack/cli/CommandFactory {
84+
public fun <init> ()V
85+
public fun create ()Lcom/github/ajalt/clikt/core/CliktCommand;
86+
public fun getDescription ()Ljava/lang/String;
87+
public fun getKey ()Ljava/lang/String;
8588
}
8689

87-
public final class slack/cli/lint/LintBaselineMergerCli$Factory : slack/cli/CommandFactory {
90+
public final class slack/cli/sarif/ApplyBaselinesToSarifs : com/github/ajalt/clikt/core/CliktCommand {
91+
public static final field DESCRIPTION Ljava/lang/String;
92+
public fun <init> ()V
93+
public fun run ()V
94+
}
95+
96+
public final class slack/cli/sarif/ApplyBaselinesToSarifs$Factory : slack/cli/CommandFactory {
8897
public fun <init> ()V
8998
public fun create ()Lcom/github/ajalt/clikt/core/CliktCommand;
9099
public fun getDescription ()Ljava/lang/String;
@@ -104,6 +113,10 @@ public final class slack/cli/sarif/MergeSarifReports$Factory : slack/cli/Command
104113
public fun getKey ()Ljava/lang/String;
105114
}
106115

116+
public synthetic class slack/cli/sarif/SarifUtilKt$EntriesMappings {
117+
public static final synthetic field entries$0 Lkotlin/enums/EnumEntries;
118+
}
119+
107120
public final class slack/cli/shellsentry/AnalysisResult {
108121
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lslack/cli/shellsentry/RetrySignal;ILkotlin/jvm/functions/Function1;)V
109122
public final fun component1 ()Ljava/lang/String;

src/main/kotlin/slack/cli/lint/LintBaselineMergerCli.kt

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import com.github.ajalt.clikt.parameters.options.default
2020
import com.github.ajalt.clikt.parameters.options.flag
2121
import com.github.ajalt.clikt.parameters.options.option
2222
import com.github.ajalt.clikt.parameters.options.required
23-
import com.github.ajalt.clikt.parameters.types.enum
2423
import com.github.ajalt.clikt.parameters.types.path
2524
import com.google.auto.service.AutoService
2625
import com.tickaroo.tikxml.converter.htmlescape.StringEscapeUtils
@@ -37,6 +36,7 @@ import io.github.detekt.sarif4k.ReportingDescriptor
3736
import io.github.detekt.sarif4k.Result
3837
import io.github.detekt.sarif4k.Run
3938
import io.github.detekt.sarif4k.SarifSchema210
39+
import io.github.detekt.sarif4k.SarifSerializer
4040
import io.github.detekt.sarif4k.Tool
4141
import io.github.detekt.sarif4k.ToolComponent
4242
import io.github.detekt.sarif4k.Version
@@ -48,33 +48,26 @@ import kotlin.io.path.name
4848
import kotlin.io.path.readText
4949
import kotlin.io.path.relativeTo
5050
import kotlin.io.path.writeText
51-
import kotlinx.serialization.ExperimentalSerializationApi
5251
import kotlinx.serialization.KSerializer
5352
import kotlinx.serialization.Serializable
5453
import kotlinx.serialization.descriptors.PrimitiveKind
5554
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
5655
import kotlinx.serialization.descriptors.SerialDescriptor
5756
import kotlinx.serialization.encoding.Decoder
5857
import kotlinx.serialization.encoding.Encoder
59-
import kotlinx.serialization.json.Json
6058
import kotlinx.serialization.serializer
6159
import nl.adaptivity.xmlutil.serialization.XML
6260
import nl.adaptivity.xmlutil.serialization.XmlSerialName
6361
import slack.cli.CommandFactory
6462
import slack.cli.projectDirOption
63+
import slack.cli.sarif.BASELINE_SUPPRESSION
64+
import slack.cli.sarif.levelOption
6565
import slack.cli.skipBuildAndCacheDirs
6666

6767
/** A CLI that merges lint baseline xml files into one. */
6868
public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
6969
private companion object {
7070
const val DESCRIPTION = "Merges multiple lint baselines into one"
71-
private val LEVEL_NAMES =
72-
Level.entries.joinToString(
73-
separator = ", ",
74-
prefix = "[",
75-
postfix = "]",
76-
transform = Level::name
77-
)
7871
}
7972

8073
@AutoService(CommandFactory::class)
@@ -102,19 +95,10 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
10295
)
10396
.default("{message}")
10497

105-
private val level by
106-
option("--level", "-l", help = "Priority level. Defaults to Error. Options are $LEVEL_NAMES")
107-
.enum<Level>()
108-
.default(Level.Error)
98+
private val level by levelOption().default(Level.Error)
10999

110100
private val verbose by option("--verbose", "-v").flag()
111101

112-
@OptIn(ExperimentalSerializationApi::class)
113-
private val json = Json {
114-
prettyPrint = true
115-
prettyPrintIndent = " "
116-
}
117-
118102
private val xml = XML { defaultPolicy { ignoreUnknownChildren() } }
119103

120104
override fun run() {
@@ -166,6 +150,7 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
166150
level = level,
167151
ruleIndex = ruleIndices.getValue(id),
168152
locations = listOf(issue.toLocation(projectPath)),
153+
suppressions = listOf(BASELINE_SUPPRESSION),
169154
message =
170155
Message(
171156
text =
@@ -177,7 +162,7 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
177162
)
178163
)
179164

180-
json.encodeToString(SarifSchema210.serializer(), outputSarif).let { outputFile.writeText(it) }
165+
SarifSerializer.toJson(outputSarif).let { outputFile.writeText(it) }
181166
}
182167

183168
private fun parseIssues(): Map<LintIssues.LintIssue, Path> {
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Copyright (C) 2023 Slack Technologies, LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package slack.cli.sarif
17+
18+
import com.github.ajalt.clikt.core.CliktCommand
19+
import com.github.ajalt.clikt.parameters.options.flag
20+
import com.github.ajalt.clikt.parameters.options.option
21+
import com.github.ajalt.clikt.parameters.options.required
22+
import com.github.ajalt.clikt.parameters.types.enum
23+
import com.github.ajalt.clikt.parameters.types.path
24+
import com.google.auto.service.AutoService
25+
import io.github.detekt.sarif4k.BaselineState
26+
import io.github.detekt.sarif4k.SarifSchema210
27+
import io.github.detekt.sarif4k.SarifSerializer
28+
import kotlin.io.path.readText
29+
import kotlin.io.path.writeText
30+
import kotlin.system.exitProcess
31+
import slack.cli.CommandFactory
32+
33+
/** A CLI that applies baselines data to a SARIF file. See the docs on [Mode] for more details. */
34+
public class ApplyBaselinesToSarifs : CliktCommand(help = DESCRIPTION) {
35+
36+
@AutoService(CommandFactory::class)
37+
public class Factory : CommandFactory {
38+
override val key: String = "apply-baselines-to-sarifs"
39+
override val description: String = DESCRIPTION
40+
41+
override fun create(): CliktCommand = ApplyBaselinesToSarifs()
42+
}
43+
44+
private companion object {
45+
const val DESCRIPTION = "A CLI that applies baselines data to a SARIF file."
46+
}
47+
48+
private val baseline by
49+
option("--baseline", "-b", help = "The baseline SARIF file to use.")
50+
.path(mustExist = true, canBeDir = false)
51+
.required()
52+
53+
private val current by
54+
option("--current", "-c", help = "The SARIF file to apply the baseline to.")
55+
.path(mustExist = true, canBeDir = false)
56+
.required()
57+
58+
private val output by
59+
option("--output", "-o", help = "The output SARIF file to write.")
60+
.path(canBeDir = false)
61+
.required()
62+
63+
private val removeUriPrefixes by
64+
option(
65+
"--remove-uri-prefixes",
66+
help =
67+
"When enabled, removes the root project directory from location uris such that they are only " +
68+
"relative to the root project dir."
69+
)
70+
.flag()
71+
72+
private val mode by
73+
option("--mode", "-m", help = "The mode to run in.").enum<Mode>(ignoreCase = true).required()
74+
75+
private val includeAbsent by
76+
option("--include-absent", "-a", help = "Include absent results in updating.").flag()
77+
78+
override fun run() {
79+
if (includeAbsent && mode != Mode.UPDATE) {
80+
echo("--include-absent can only be used with --mode=update", err = true)
81+
exitProcess(1)
82+
}
83+
val baseline = SarifSerializer.fromJson(baseline.readText())
84+
val sarifToUpdate = SarifSerializer.fromJson(current.readText())
85+
86+
val updatedSarif = sarifToUpdate.applyBaseline(baseline)
87+
88+
output.writeText(SarifSerializer.toJson(updatedSarif))
89+
}
90+
91+
@Suppress("LongMethod")
92+
private fun SarifSchema210.applyBaseline(baseline: SarifSchema210): SarifSchema210 {
93+
// Assume a single run for now
94+
val results = runs.first().results!!
95+
val baselineResults = baseline.runs.first().results!!
96+
97+
val suppressions = listOf(BASELINE_SUPPRESSION)
98+
99+
return when (mode) {
100+
Mode.MERGE -> {
101+
// Mark baselines as suppressed and no baseline state
102+
val suppressedBaselineSchema =
103+
baseline.copy(
104+
runs =
105+
baseline.runs.map { run ->
106+
run.copy(
107+
results =
108+
baselineResults.map {
109+
it.copy(baselineState = null, suppressions = suppressions)
110+
}
111+
)
112+
}
113+
)
114+
// Mark new results as new and not suppressed
115+
val newSchema =
116+
copy(
117+
runs =
118+
runs.map { run ->
119+
run.copy(
120+
results =
121+
results.map {
122+
it.copy(baselineState = BaselineState.New, suppressions = emptyList())
123+
}
124+
)
125+
}
126+
)
127+
// Merge the two
128+
listOf(suppressedBaselineSchema, newSchema)
129+
.merge(removeUriPrefixes = removeUriPrefixes, log = ::echo)
130+
}
131+
Mode.UPDATE -> {
132+
val baselineResultsByHash = baselineResults.associateBy { it.identityHash }
133+
val resultsByHash = results.associateBy { it.identityHash }
134+
// New -> No match in the baseline
135+
// Unchanged -> Exact match in the baseline.
136+
// Updated -> Partial match is found. Not sure if we could realistically detect this well
137+
// based on just ID and location though. May be that the only change we could
138+
// match here would be if the severity changes
139+
// Absent -> Nothing to report, means this issue was fixed presumably. Not sure how this
140+
// would show up in a baseline state tbh
141+
val baselinedResults =
142+
results.map { result ->
143+
val baselineResult = baselineResultsByHash[result.identityHash]
144+
when {
145+
baselineResult == null -> {
146+
// No baseline result, so it's new!
147+
result.copy(baselineState = BaselineState.New)
148+
}
149+
baselineResult.shallowHash == result.shallowHash -> {
150+
// They're they same, so it's unchanged
151+
result.copy(baselineState = BaselineState.Unchanged, suppressions = suppressions)
152+
}
153+
else -> {
154+
// They're different, so it's updated
155+
result.copy(baselineState = BaselineState.Updated, suppressions = suppressions)
156+
}
157+
}
158+
}
159+
val absentResults =
160+
if (includeAbsent) {
161+
// Create a copy of the baseline results that are absent with a suppression
162+
baselineResults
163+
.filter { result -> result.identityHash !in resultsByHash }
164+
.map { it.copy(baselineState = BaselineState.Absent, suppressions = suppressions) }
165+
} else {
166+
emptyList()
167+
}
168+
val absentResultsSchema =
169+
baseline.copy(runs = runs.map { run -> run.copy(results = absentResults) })
170+
val newCurrentSchema = copy(runs = runs.map { run -> run.copy(results = baselinedResults) })
171+
172+
newCurrentSchema.mergeWith(
173+
absentResultsSchema,
174+
removeUriPrefixes = removeUriPrefixes,
175+
log = ::echo
176+
)
177+
}
178+
}
179+
}
180+
181+
internal enum class Mode {
182+
/**
183+
* Merge two SARIFs, this does the following:
184+
* - Marks the baseline results as "suppressed".
185+
* - Marks the new results as "new".
186+
*
187+
* The two SARIFs are deemed to be distinct results and have no overlaps.
188+
*/
189+
MERGE,
190+
/**
191+
* Update the input SARIF based on a previous baseline:
192+
* - Marks the new results as "new".
193+
* - Marks the absent results as "absent" (aka "fixed").
194+
* - Mark remaining as updated or unchanged.
195+
* - No changes are made to suppressions.
196+
*/
197+
UPDATE,
198+
}
199+
}

0 commit comments

Comments
 (0)