Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions invert-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {

implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation("io.github.detekt.sarif4k:sarif4k:0.6.0")

testImplementation(libs.kotlin.test)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.squareup.invert.StatCollector
import com.squareup.invert.internal.models.CollectedStatsForProject
import com.squareup.invert.internal.models.InvertCombinedCollectedData
import com.squareup.invert.internal.report.json.InvertJsonReportWriter
import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter
import com.squareup.invert.models.ExtraDataType
import com.squareup.invert.models.ExtraMetadata
import com.squareup.invert.models.ModulePath
Expand Down Expand Up @@ -99,6 +100,16 @@ object CollectedStatAggregator {
values = allCodeReferencesForStatWithProjectPathExtra
)
)

InvertSarifReportWriter.writeToSarifReport(
description = "All CodeReferences for ${statMetadata.key}",
fileName = InvertFileUtils.outputFile(
File(reportOutputConfig.invertReportDirectory, "sarif"),
"code_references_${statMetadata.key}.sarif"
),
metadata = statMetadata,
values = allCodeReferencesForStatWithProjectPathExtra
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ object InvertFileUtils {
const val INVERT_FOLDER_NAME = "invert"
const val JS_FOLDER_NAME = "js"
const val JSON_FOLDER_NAME = "json"
const val SARIF_FOLDER_NAME = "sarif"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


val REPORTS_SLASH_INVERT_PATH = REPORTS_FOLDER_NAME.addSlashAnd(INVERT_FOLDER_NAME)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ enum class InvertPluginFileKey(
OWNERS("owners.json", "Owners"),
METADATA("metadata.json", "Metadata"),
STATS("stats.json", "Stats"),
STATS_SARIF("stats.sarif", "Stats"),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, i thought i deleted stats.json, but maybe that was only stats.js. This is fine but the concern is that the single file holds all data and that doesn't scale well. For the js/web we were forced to do that to collect things like all kotlin files. This can merge, but we should discuss how to get rid of those mega files eventually

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we are splitting it up as well, but I saw that json was doing one whole file so I followed suit

if we don’t need this then I can remove for sure

STAT_TOTALS("stat_totals.json", "Stat Totals"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.squareup.invert.internal.report.js.InvertJsReportUtils
import com.squareup.invert.internal.report.js.InvertJsReportUtils.computeGlobalTotals
import com.squareup.invert.internal.report.js.InvertJsReportWriter
import com.squareup.invert.internal.report.json.InvertJsonReportWriter
import com.squareup.invert.internal.report.sarif.InvertSarifReportWriter
import com.squareup.invert.logging.InvertLogger
import com.squareup.invert.models.DependencyId
import com.squareup.invert.models.ModulePath
Expand Down Expand Up @@ -61,6 +62,11 @@ class InvertReportWriter(
historicalData = historicalDataWithCurrent,
)

// Include all stats into one SARIF report.
InvertSarifReportWriter(invertLogger, rootBuildReportsDir).createInvertSarifReport(
allProjectsStatsData = allProjectsStatsData
)

// HTML/JS Report
InvertJsReportWriter(invertLogger, rootBuildReportsDir).createInvertHtmlReport(
reportMetadata = reportMetadata,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package com.squareup.invert.internal.report.sarif

import com.squareup.invert.internal.InvertFileUtils
import com.squareup.invert.internal.models.InvertPluginFileKey
import com.squareup.invert.internal.report.json.InvertJsonReportWriter.Companion.writeJsonFile
import com.squareup.invert.logging.InvertLogger
import com.squareup.invert.models.ModulePath
import com.squareup.invert.models.Stat
import com.squareup.invert.models.StatKey
import com.squareup.invert.models.StatMetadata
import com.squareup.invert.models.js.StatsJsReportModel
import io.github.detekt.sarif4k.ArtifactLocation
import io.github.detekt.sarif4k.Location
import io.github.detekt.sarif4k.Message
import io.github.detekt.sarif4k.MultiformatMessageString
import io.github.detekt.sarif4k.PhysicalLocation
import io.github.detekt.sarif4k.PropertyBag
import io.github.detekt.sarif4k.Region
import io.github.detekt.sarif4k.ReportingDescriptor
import io.github.detekt.sarif4k.ReportingDescriptorReference
import io.github.detekt.sarif4k.Result as SarifResult
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
import kotlinx.serialization.KSerializer
import java.io.File
import java.nio.file.Files

/**
* Writer for generating SARIF (Static Analysis Results Interchange Format) reports for Invert.
* SARIF is a standard format for static analysis tools to report their results.
*/
class InvertSarifReportWriter(
private val logger: InvertLogger,
rootBuildReportsDir: File,
) {
private val rootBuildSarifReportsDir = File(rootBuildReportsDir, InvertFileUtils.SARIF_FOLDER_NAME)

/**
* Writes a SARIF report to a file.
*/
private fun writeToSarif(
jsonFileKey: InvertPluginFileKey,
rulesAndResults: Map<ReportingDescriptor, List<SarifResult>>
) {
val sarif = createSarifSchema(
toolName = "Invert",
toolVersion = "1.0.0",
rule = rulesAndResults.keys.toList(),
results = rulesAndResults.values.flatten()
)


val sarifFile = InvertFileUtils.outputFile(
directory = rootBuildSarifReportsDir,
filename = jsonFileKey.filename
)

Files.write(sarifFile.toPath(), SarifSerializer.toMinifiedJson(sarif).toByteArray())
}

/**
* Extension function to convert StatsJsReportModel to a map of SARIF rules to their results.
*/
private fun StatsJsReportModel.asSarifRulesAndResults(): Map<ReportingDescriptor, List<SarifResult>> {
val rulesAndResults = mutableMapOf<ReportingDescriptor, MutableList<SarifResult>>()

statsByModule.forEach { (modulePath, statMap) ->
statMap.forEach { (statKey, stat) ->
// Get or create the rule for this stat
val rule = statInfos[statKey]?.asReportingDescriptor() ?: return@forEach

// Get or create the results list for this rule
val results = rulesAndResults.getOrPut(rule) { mutableListOf() }

// Add results for this stat
results.addAll(stat.asSarifResult(modulePath, statKey, statInfos[statKey]))
}
}

return rulesAndResults
}

/**
* Creates a SARIF report for Invert statistics and metadata.
*
* @param allProjectsStatsData Statistics data for all projects
*/
fun createInvertSarifReport(
allProjectsStatsData: StatsJsReportModel
) {
val rulesAndResults = allProjectsStatsData.asSarifRulesAndResults()
writeToSarif(
jsonFileKey = InvertPluginFileKey.STATS_SARIF,
rulesAndResults = rulesAndResults
)
}

/**
* Writes a JSON file to the specified directory.
*/
private fun <T> writeJsonFileInDir(
jsonFileKey: InvertPluginFileKey,
serializer: KSerializer<T>,
value: T,
) = writeJsonFile(
logger = logger,
jsonFileKey = jsonFileKey,
jsonOutputFile = InvertFileUtils.outputFile(
directory = rootBuildSarifReportsDir,
filename = jsonFileKey.filename
),
serializer = serializer,
value = value
)

companion object {
private const val SARIF_FILE_NAME = "invert-report.sarif"

fun writeToSarifReport(
values: List<Stat.CodeReferencesStat.CodeReference>,
metadata: StatMetadata,
fileName: File,
description: String
) {
val results = values.map { it.toSarifResult(metadata.key, modulePath = null, metadata) }
val rule = metadata.asReportingDescriptor(shortDescription = description)
val sarifSchema = createSarifSchemaFromResults(rule = rule, results = results)
val sarifJson = SarifSerializer.toMinifiedJson(sarifSchema)
fileName.writeText(sarifJson)
}
}
}

/**
* Extension function to convert Stat to SARIF results.
*/
private fun Stat.asSarifResult(
module: ModulePath,
key: StatKey,
metadata: StatMetadata?
): List<SarifResult> = when (this) {
is Stat.CodeReferencesStat -> value.map {
it.toSarifResult(
key = key, modulePath = module, metadata = metadata
)
}
// No support for other stat types in SARIF
else -> emptyList()
}

/**
* Extension function to convert CodeReference to SARIF result.
*/
private fun Stat.CodeReferencesStat.CodeReference.toSarifResult(
key: StatKey,
modulePath: ModulePath?,
metadata: StatMetadata?
): SarifResult = SarifResult(
ruleID = key,
message = Message(text = code),
locations = listOf(
Location(
physicalLocation = PhysicalLocation(
artifactLocation = ArtifactLocation(uri = filePath),
region = Region(
startLine = startLine.toLong(),
endLine = endLine.toLong(),
sourceLanguage = code?.trim()
),
properties = PropertyBag(
extras + mapOf(
"fileType" to filePath.split(".").last()
)
)
)
)
),
properties = PropertyBag(
mapOf(
"owner" to (owner ?: "Unknown"),
"module" to modulePath,
"uniqueId" to uniqueId,
)
),
rule = ReportingDescriptorReference(id = key)
)

/**
* Extension function to convert StatMetadata to SARIF reporting descriptor.
*/
private fun StatMetadata.asReportingDescriptor(shortDescription: String = ""): ReportingDescriptor =
ReportingDescriptor(
id = key,
name = this.title,
fullDescription = MultiformatMessageString(markdown = description, text = description),
shortDescription = MultiformatMessageString(text = shortDescription),
properties = PropertyBag(
mapOf(
"description" to description,
"extras" to extras,
"category" to category,
"title" to title
)
)
)

private fun createSarifSchemaFromResults(
rule: ReportingDescriptor,
results: List<SarifResult>
): SarifSchema210 {
return createSarifSchema(toolName = rule.id, rule = listOf(rule), results = results)
}

private fun createSarifSchema(
rule: List<ReportingDescriptor>,
results: List<SarifResult>,
toolName: String = "Invert",
toolVersion: String = "1.0.0",
): SarifSchema210 {
return SarifSchema210(
version = Version.The210,
schema = "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
runs = listOf(
Run(
tool = Tool(
driver = ToolComponent(
name = toolName,
version = toolVersion,
rules = rule
)
),
results = results
)
)
)

}