-
Notifications
You must be signed in to change notification settings - Fork 2
Add sarif support to Invert using sarif4k #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
df93951
ee87848
cee77a3
606dd7e
c92b8d8
9e31943
9330b64
3f0835f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
|
|
||
| val REPORTS_SLASH_INVERT_PATH = REPORTS_FOLDER_NAME.addSlashAnd(INVERT_FOLDER_NAME) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,5 +15,6 @@ enum class InvertPluginFileKey( | |
| OWNERS("owners.json", "Owners"), | ||
| METADATA("metadata.json", "Metadata"), | ||
| STATS("stats.json", "Stats"), | ||
| STATS_SARIF("stats.sarif", "Stats"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
| @@ -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", | ||
mannydelgado marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| toolVersion = "1.0.0", | ||
| rule = rulesAndResults.keys.toList(), | ||
| results = rulesAndResults.values.flatten() | ||
| ) | ||
|
|
||
mannydelgado marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() | ||
mannydelgado marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ), | ||
| 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 | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.