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

Commit

Permalink
Misc sarif updates (#80)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ZacSweers authored Dec 5, 2023
1 parent ce42036 commit 0729231
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 87 deletions.
19 changes: 16 additions & 3 deletions api/kotlin-cli-util.api
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,20 @@ public final class slack/cli/lint/LintBaselineMergerCli : com/github/ajalt/clikt
public fun run ()V
}

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

public final class slack/cli/lint/LintBaselineMergerCli$Factory : slack/cli/CommandFactory {
public final class slack/cli/sarif/ApplyBaselinesToSarifs : com/github/ajalt/clikt/core/CliktCommand {
public static final field DESCRIPTION Ljava/lang/String;
public fun <init> ()V
public fun run ()V
}

public final class slack/cli/sarif/ApplyBaselinesToSarifs$Factory : slack/cli/CommandFactory {
public fun <init> ()V
public fun create ()Lcom/github/ajalt/clikt/core/CliktCommand;
public fun getDescription ()Ljava/lang/String;
Expand All @@ -104,6 +113,10 @@ public final class slack/cli/sarif/MergeSarifReports$Factory : slack/cli/Command
public fun getKey ()Ljava/lang/String;
}

public synthetic class slack/cli/sarif/SarifUtilKt$EntriesMappings {
public static final synthetic field entries$0 Lkotlin/enums/EnumEntries;
}

public final class slack/cli/shellsentry/AnalysisResult {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lslack/cli/shellsentry/RetrySignal;ILkotlin/jvm/functions/Function1;)V
public final fun component1 ()Ljava/lang/String;
Expand Down
27 changes: 6 additions & 21 deletions src/main/kotlin/slack/cli/lint/LintBaselineMergerCli.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.path
import com.google.auto.service.AutoService
import com.tickaroo.tikxml.converter.htmlescape.StringEscapeUtils
Expand All @@ -37,6 +36,7 @@ import io.github.detekt.sarif4k.ReportingDescriptor
import io.github.detekt.sarif4k.Result
import io.github.detekt.sarif4k.Run
import io.github.detekt.sarif4k.SarifSchema210
import io.github.detekt.sarif4k.SarifSerializer
import io.github.detekt.sarif4k.Tool
import io.github.detekt.sarif4k.ToolComponent
import io.github.detekt.sarif4k.Version
Expand All @@ -48,33 +48,26 @@ import kotlin.io.path.name
import kotlin.io.path.readText
import kotlin.io.path.relativeTo
import kotlin.io.path.writeText
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import nl.adaptivity.xmlutil.serialization.XML
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import slack.cli.CommandFactory
import slack.cli.projectDirOption
import slack.cli.sarif.BASELINE_SUPPRESSION
import slack.cli.sarif.levelOption
import slack.cli.skipBuildAndCacheDirs

/** A CLI that merges lint baseline xml files into one. */
public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
private companion object {
const val DESCRIPTION = "Merges multiple lint baselines into one"
private val LEVEL_NAMES =
Level.entries.joinToString(
separator = ", ",
prefix = "[",
postfix = "]",
transform = Level::name
)
}

@AutoService(CommandFactory::class)
Expand Down Expand Up @@ -102,19 +95,10 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
)
.default("{message}")

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

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

@OptIn(ExperimentalSerializationApi::class)
private val json = Json {
prettyPrint = true
prettyPrintIndent = " "
}

private val xml = XML { defaultPolicy { ignoreUnknownChildren() } }

override fun run() {
Expand Down Expand Up @@ -166,6 +150,7 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
level = level,
ruleIndex = ruleIndices.getValue(id),
locations = listOf(issue.toLocation(projectPath)),
suppressions = listOf(BASELINE_SUPPRESSION),
message =
Message(
text =
Expand All @@ -177,7 +162,7 @@ public class LintBaselineMergerCli : CliktCommand(DESCRIPTION) {
)
)

json.encodeToString(SarifSchema210.serializer(), outputSarif).let { outputFile.writeText(it) }
SarifSerializer.toJson(outputSarif).let { outputFile.writeText(it) }
}

private fun parseIssues(): Map<LintIssues.LintIssue, Path> {
Expand Down
199 changes: 199 additions & 0 deletions src/main/kotlin/slack/cli/sarif/ApplyBaselinesToSarifs.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/*
* Copyright (C) 2023 Slack Technologies, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package slack.cli.sarif

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.path
import com.google.auto.service.AutoService
import io.github.detekt.sarif4k.BaselineState
import io.github.detekt.sarif4k.SarifSchema210
import io.github.detekt.sarif4k.SarifSerializer
import kotlin.io.path.readText
import kotlin.io.path.writeText
import kotlin.system.exitProcess
import slack.cli.CommandFactory

/** A CLI that applies baselines data to a SARIF file. See the docs on [Mode] for more details. */
public class ApplyBaselinesToSarifs : CliktCommand(help = DESCRIPTION) {

@AutoService(CommandFactory::class)
public class Factory : CommandFactory {
override val key: String = "apply-baselines-to-sarifs"
override val description: String = DESCRIPTION

override fun create(): CliktCommand = ApplyBaselinesToSarifs()
}

private companion object {
const val DESCRIPTION = "A CLI that applies baselines data to a SARIF file."
}

private val baseline by
option("--baseline", "-b", help = "The baseline SARIF file to use.")
.path(mustExist = true, canBeDir = false)
.required()

private val current by
option("--current", "-c", help = "The SARIF file to apply the baseline to.")
.path(mustExist = true, canBeDir = false)
.required()

private val output by
option("--output", "-o", help = "The output SARIF file to write.")
.path(canBeDir = false)
.required()

private val removeUriPrefixes by
option(
"--remove-uri-prefixes",
help =
"When enabled, removes the root project directory from location uris such that they are only " +
"relative to the root project dir."
)
.flag()

private val mode by
option("--mode", "-m", help = "The mode to run in.").enum<Mode>(ignoreCase = true).required()

private val includeAbsent by
option("--include-absent", "-a", help = "Include absent results in updating.").flag()

override fun run() {
if (includeAbsent && mode != Mode.UPDATE) {
echo("--include-absent can only be used with --mode=update", err = true)
exitProcess(1)
}
val baseline = SarifSerializer.fromJson(baseline.readText())
val sarifToUpdate = SarifSerializer.fromJson(current.readText())

val updatedSarif = sarifToUpdate.applyBaseline(baseline)

output.writeText(SarifSerializer.toJson(updatedSarif))
}

@Suppress("LongMethod")
private fun SarifSchema210.applyBaseline(baseline: SarifSchema210): SarifSchema210 {
// Assume a single run for now
val results = runs.first().results!!
val baselineResults = baseline.runs.first().results!!

val suppressions = listOf(BASELINE_SUPPRESSION)

return when (mode) {
Mode.MERGE -> {
// Mark baselines as suppressed and no baseline state
val suppressedBaselineSchema =
baseline.copy(
runs =
baseline.runs.map { run ->
run.copy(
results =
baselineResults.map {
it.copy(baselineState = null, suppressions = suppressions)
}
)
}
)
// Mark new results as new and not suppressed
val newSchema =
copy(
runs =
runs.map { run ->
run.copy(
results =
results.map {
it.copy(baselineState = BaselineState.New, suppressions = emptyList())
}
)
}
)
// Merge the two
listOf(suppressedBaselineSchema, newSchema)
.merge(removeUriPrefixes = removeUriPrefixes, log = ::echo)
}
Mode.UPDATE -> {
val baselineResultsByHash = baselineResults.associateBy { it.identityHash }
val resultsByHash = results.associateBy { it.identityHash }
// New -> No match in the baseline
// Unchanged -> Exact match in the baseline.
// Updated -> Partial match is found. Not sure if we could realistically detect this well
// based on just ID and location though. May be that the only change we could
// match here would be if the severity changes
// Absent -> Nothing to report, means this issue was fixed presumably. Not sure how this
// would show up in a baseline state tbh
val baselinedResults =
results.map { result ->
val baselineResult = baselineResultsByHash[result.identityHash]
when {
baselineResult == null -> {
// No baseline result, so it's new!
result.copy(baselineState = BaselineState.New)
}
baselineResult.shallowHash == result.shallowHash -> {
// They're they same, so it's unchanged
result.copy(baselineState = BaselineState.Unchanged, suppressions = suppressions)
}
else -> {
// They're different, so it's updated
result.copy(baselineState = BaselineState.Updated, suppressions = suppressions)
}
}
}
val absentResults =
if (includeAbsent) {
// Create a copy of the baseline results that are absent with a suppression
baselineResults
.filter { result -> result.identityHash !in resultsByHash }
.map { it.copy(baselineState = BaselineState.Absent, suppressions = suppressions) }
} else {
emptyList()
}
val absentResultsSchema =
baseline.copy(runs = runs.map { run -> run.copy(results = absentResults) })
val newCurrentSchema = copy(runs = runs.map { run -> run.copy(results = baselinedResults) })

newCurrentSchema.mergeWith(
absentResultsSchema,
removeUriPrefixes = removeUriPrefixes,
log = ::echo
)
}
}
}

internal enum class Mode {
/**
* Merge two SARIFs, this does the following:
* - Marks the baseline results as "suppressed".
* - Marks the new results as "new".
*
* The two SARIFs are deemed to be distinct results and have no overlaps.
*/
MERGE,
/**
* Update the input SARIF based on a previous baseline:
* - Marks the new results as "new".
* - Marks the absent results as "absent" (aka "fixed").
* - Mark remaining as updated or unchanged.
* - No changes are made to suppressions.
*/
UPDATE,
}
}
Loading

0 comments on commit 0729231

Please sign in to comment.