|
| 1 | +#r "nuget: Thoth.Json.Newtonsoft, 0.3.2" |
| 2 | + |
| 3 | +open System |
| 4 | +open System.IO |
| 5 | +open System.Threading.Tasks |
| 6 | + |
| 7 | +type Text = { text: string } |
| 8 | + |
| 9 | +type Region = |
| 10 | + { startLine: int |
| 11 | + startColumn: int |
| 12 | + endLine: int |
| 13 | + endColumn: int } |
| 14 | + |
| 15 | +type ArtifactLocation = { uri: string } |
| 16 | + |
| 17 | +type PhysicalLocation = |
| 18 | + { artifactLocation: ArtifactLocation |
| 19 | + region: Region } |
| 20 | + |
| 21 | +type Location = { physicalLocation: PhysicalLocation } |
| 22 | + |
| 23 | +type Message = { text: string } |
| 24 | + |
| 25 | +type Result = |
| 26 | + { ruleId: string |
| 27 | + ruleIndex: int |
| 28 | + message: Message |
| 29 | + locations: Location list } |
| 30 | + |
| 31 | +type RuleShortDescription = { text: string; markdown: string } |
| 32 | + |
| 33 | +type Rule = |
| 34 | + { id: string |
| 35 | + name: string |
| 36 | + shortDescription: RuleShortDescription |
| 37 | + helpUri: string } |
| 38 | + |
| 39 | +type Driver = |
| 40 | + { name: string |
| 41 | + version: string |
| 42 | + informationUri: string |
| 43 | + rules: Rule list option } |
| 44 | + |
| 45 | +type Tool = { driver: Driver } |
| 46 | + |
| 47 | +type Invocation = |
| 48 | + { startTimeUtc: DateTime |
| 49 | + endTimeUtc: DateTime |
| 50 | + executionSuccessful: bool } |
| 51 | + |
| 52 | +type Run = |
| 53 | + { results: Result list |
| 54 | + tool: Tool |
| 55 | + invocations: Invocation list |
| 56 | + columnKind: string } |
| 57 | + |
| 58 | +type SarifLog = |
| 59 | + { schema: string |
| 60 | + version: string |
| 61 | + runs: Run list } |
| 62 | + |
| 63 | +module private Encoders = |
| 64 | + open Thoth.Json.Core |
| 65 | + |
| 66 | + let textEncoder: Encoder<Text> = |
| 67 | + fun (t: Text) -> Encode.object [ ("text", Encode.string t.text) ] |
| 68 | + |
| 69 | + let regionEncoder: Encoder<Region> = |
| 70 | + fun (r: Region) -> |
| 71 | + Encode.object |
| 72 | + [ ("startLine", Encode.int r.startLine) |
| 73 | + ("startColumn", Encode.int r.startColumn) |
| 74 | + ("endLine", Encode.int r.endLine) |
| 75 | + ("endColumn", Encode.int r.endColumn) ] |
| 76 | + |
| 77 | + let artifactLocationEncoder: Encoder<ArtifactLocation> = |
| 78 | + fun (al: ArtifactLocation) -> Encode.object [ ("uri", Encode.string al.uri) ] |
| 79 | + |
| 80 | + let physicalLocationEncoder: Encoder<PhysicalLocation> = |
| 81 | + fun (pl: PhysicalLocation) -> |
| 82 | + Encode.object |
| 83 | + [ ("artifactLocation", artifactLocationEncoder pl.artifactLocation) |
| 84 | + ("region", regionEncoder pl.region) ] |
| 85 | + |
| 86 | + let locationEncoder: Encoder<Location> = |
| 87 | + fun (l: Location) -> Encode.object [ ("physicalLocation", physicalLocationEncoder l.physicalLocation) ] |
| 88 | + |
| 89 | + let messageEncoder: Encoder<Message> = |
| 90 | + fun (m: Message) -> Encode.object [ ("text", Encode.string m.text) ] |
| 91 | + |
| 92 | + let resultEncoder: Encoder<Result> = |
| 93 | + fun (r: Result) -> |
| 94 | + Encode.object |
| 95 | + [ ("ruleId", Encode.string r.ruleId) |
| 96 | + ("ruleIndex", Encode.int r.ruleIndex) |
| 97 | + ("message", messageEncoder r.message) |
| 98 | + ("locations", List.map locationEncoder r.locations |> Encode.list) ] |
| 99 | + |
| 100 | + let ruleShortDescriptionEncoder: Encoder<RuleShortDescription> = |
| 101 | + fun (rsd: RuleShortDescription) -> |
| 102 | + Encode.object [ ("text", Encode.string rsd.text); ("markdown", Encode.string rsd.markdown) ] |
| 103 | + |
| 104 | + let ruleEncoder: Encoder<Rule> = |
| 105 | + fun (r: Rule) -> |
| 106 | + Encode.object |
| 107 | + [ ("id", Encode.string r.id) |
| 108 | + ("name", Encode.string r.name) |
| 109 | + ("shortDescription", ruleShortDescriptionEncoder r.shortDescription) |
| 110 | + ("helpUri", Encode.string r.helpUri) ] |
| 111 | + |
| 112 | + let driverEncoder: Encoder<Driver> = |
| 113 | + fun (d: Driver) -> |
| 114 | + Encode.object |
| 115 | + [ ("name", Encode.string d.name) |
| 116 | + ("version", Encode.string d.version) |
| 117 | + ("informationUri", Encode.string d.informationUri) |
| 118 | + ("rules", |
| 119 | + match d.rules with |
| 120 | + | None -> Encode.list [] |
| 121 | + | Some rules -> List.map ruleEncoder rules |> Encode.list) ] |
| 122 | + |
| 123 | + let toolEncoder: Encoder<Tool> = |
| 124 | + fun (t: Tool) -> Encode.object [ ("driver", driverEncoder t.driver) ] |
| 125 | + |
| 126 | + let invocationEncoder: Encoder<Invocation> = |
| 127 | + fun (i: Invocation) -> |
| 128 | + Encode.object |
| 129 | + [ ("startTimeUtc", Encode.string (i.startTimeUtc.ToString("o"))) // ISO 8601 format |
| 130 | + ("endTimeUtc", Encode.string (i.endTimeUtc.ToString("o"))) |
| 131 | + ("executionSuccessful", Encode.bool i.executionSuccessful) ] |
| 132 | + |
| 133 | + let runEncoder: Encoder<Run> = |
| 134 | + fun (r: Run) -> |
| 135 | + Encode.object |
| 136 | + [ ("results", List.map resultEncoder r.results |> Encode.list) |
| 137 | + ("tool", toolEncoder r.tool) |
| 138 | + ("invocations", List.map invocationEncoder r.invocations |> Encode.list) |
| 139 | + ("columnKind", Encode.string r.columnKind) ] |
| 140 | + |
| 141 | + let sarifLogEncoder: Encoder<SarifLog> = |
| 142 | + fun (log: SarifLog) -> |
| 143 | + Encode.object |
| 144 | + [ ("$schema", Encode.string log.schema) |
| 145 | + ("version", Encode.string log.version) |
| 146 | + ("runs", List.map runEncoder log.runs |> Encode.list) ] |
| 147 | + |
| 148 | +module private Decoders = |
| 149 | + open Thoth.Json.Core |
| 150 | + |
| 151 | + let textDecoder: Decoder<Text> = |
| 152 | + Decode.object (fun get -> { text = get.Required.Field "text" Decode.string }) |
| 153 | + |
| 154 | + let regionDecoder: Decoder<Region> = |
| 155 | + Decode.object (fun get -> |
| 156 | + { startLine = get.Required.Field "startLine" Decode.int |
| 157 | + startColumn = get.Required.Field "startColumn" Decode.int |
| 158 | + endLine = get.Required.Field "endLine" Decode.int |
| 159 | + endColumn = get.Required.Field "endColumn" Decode.int }) |
| 160 | + |
| 161 | + let artifactLocationDecoder: Decoder<ArtifactLocation> = |
| 162 | + Decode.object (fun get -> { uri = get.Required.Field "uri" Decode.string }) |
| 163 | + |
| 164 | + let physicalLocationDecoder: Decoder<PhysicalLocation> = |
| 165 | + Decode.object (fun get -> |
| 166 | + { artifactLocation = get.Required.Field "artifactLocation" artifactLocationDecoder |
| 167 | + region = get.Required.Field "region" regionDecoder }) |
| 168 | + |
| 169 | + let locationDecoder: Decoder<Location> = |
| 170 | + Decode.object (fun get -> { physicalLocation = get.Required.Field "physicalLocation" physicalLocationDecoder }) |
| 171 | + |
| 172 | + let messageDecoder: Decoder<Message> = |
| 173 | + Decode.object (fun get -> { text = get.Required.Field "text" Decode.string }) |
| 174 | + |
| 175 | + let resultDecoder: Decoder<Result> = |
| 176 | + Decode.object (fun get -> |
| 177 | + { ruleId = get.Required.Field "ruleId" Decode.string |
| 178 | + ruleIndex = get.Required.Field "ruleIndex" Decode.int |
| 179 | + message = get.Required.Field "message" messageDecoder |
| 180 | + locations = get.Required.Field "locations" (Decode.list locationDecoder) }) |
| 181 | + |
| 182 | + let ruleShortDescriptionDecoder: Decoder<RuleShortDescription> = |
| 183 | + Decode.object (fun get -> |
| 184 | + { text = get.Required.Field "text" Decode.string |
| 185 | + markdown = get.Required.Field "markdown" Decode.string }) |
| 186 | + |
| 187 | + let ruleDecoder: Decoder<Rule> = |
| 188 | + Decode.object (fun get -> |
| 189 | + { id = get.Required.Field "id" Decode.string |
| 190 | + name = get.Required.Field "name" Decode.string |
| 191 | + shortDescription = get.Required.Field "shortDescription" ruleShortDescriptionDecoder |
| 192 | + helpUri = get.Required.Field "helpUri" Decode.string }) |
| 193 | + |
| 194 | + let driverDecoder: Decoder<Driver> = |
| 195 | + Decode.object (fun get -> |
| 196 | + { name = get.Required.Field "name" Decode.string |
| 197 | + version = get.Required.Field "version" Decode.string |
| 198 | + informationUri = get.Required.Field "informationUri" Decode.string |
| 199 | + rules = get.Optional.Field "rules" (Decode.list ruleDecoder) }) |
| 200 | + |
| 201 | + let toolDecoder: Decoder<Tool> = |
| 202 | + Decode.object (fun get -> { driver = get.Required.Field "driver" driverDecoder }) |
| 203 | + |
| 204 | + let invocationDecoder: Decoder<Invocation> = |
| 205 | + Decode.object (fun get -> |
| 206 | + { startTimeUtc = get.Required.Field "startTimeUtc" Decode.datetimeUtc |
| 207 | + endTimeUtc = get.Required.Field "endTimeUtc" Decode.datetimeUtc |
| 208 | + executionSuccessful = get.Required.Field "executionSuccessful" Decode.bool }) |
| 209 | + |
| 210 | + let runDecoder: Decoder<Run> = |
| 211 | + Decode.object (fun get -> |
| 212 | + { results = get.Required.Field "results" (Decode.list resultDecoder) |
| 213 | + tool = get.Required.Field "tool" toolDecoder |
| 214 | + invocations = get.Required.Field "invocations" (Decode.list invocationDecoder) |
| 215 | + columnKind = get.Required.Field "columnKind" Decode.string }) |
| 216 | + |
| 217 | + let sarifLogDecoder: Decoder<SarifLog> = |
| 218 | + Decode.object (fun get -> |
| 219 | + { schema = get.Required.Field "$schema" Decode.string |
| 220 | + version = get.Required.Field "version" Decode.string |
| 221 | + runs = get.Required.Field "runs" (Decode.list runDecoder) }) |
| 222 | + |
| 223 | +let private readSarif (json: string) : Result<SarifLog, string> = |
| 224 | + match Thoth.Json.Newtonsoft.Decode.fromString Decoders.sarifLogDecoder json with |
| 225 | + | Ok sarifLog -> Ok sarifLog |
| 226 | + | Error err -> Error($"Failed to decode, got %A{err}") |
| 227 | + |
| 228 | +let private writeSarif (sarifLog: SarifLog) : string = |
| 229 | + Encoders.sarifLogEncoder sarifLog |> Thoth.Json.Newtonsoft.Encode.toString 4 |
| 230 | + |
| 231 | +let mergeSarifFiles _ = |
| 232 | + task { |
| 233 | + let mergedPath = |
| 234 | + Path.Combine(__SOURCE_DIRECTORY__, "analysisreports", "merged.sarif") |
| 235 | + |
| 236 | + if Path.Exists(mergedPath) then |
| 237 | + File.Delete(mergedPath) |
| 238 | + |
| 239 | + let! sarifFiles = |
| 240 | + Directory.GetFiles("analysisreports", "*.sarif") |
| 241 | + |> Seq.map (fun path -> |
| 242 | + task { |
| 243 | + let! sarifContent = File.ReadAllTextAsync(path) |
| 244 | + let sarifResult = readSarif sarifContent |
| 245 | + |
| 246 | + match sarifResult with |
| 247 | + | Error e -> |
| 248 | + eprintfn $"%A{e}" |
| 249 | + return exit 1 |
| 250 | + | Ok sarif -> return path, sarif |
| 251 | + }) |
| 252 | + |> Task.WhenAll |
| 253 | + |
| 254 | + if Array.isEmpty sarifFiles then |
| 255 | + printfn "No sarif files could be merged" |
| 256 | + else |
| 257 | + let firstSarif = snd sarifFiles.[0] |
| 258 | + let firstRun = firstSarif.runs.[0] |
| 259 | + |
| 260 | + let results = ResizeArray() |
| 261 | + let rules = ResizeArray() |
| 262 | + |
| 263 | + for _, sarif in sarifFiles do |
| 264 | + for run in sarif.runs do |
| 265 | + results.AddRange(run.results) |
| 266 | + |
| 267 | + match run.tool.driver.rules with |
| 268 | + | None -> () |
| 269 | + | Some rulesList -> rules.AddRange(rulesList) |
| 270 | + |
| 271 | + |
| 272 | + let combined: SarifLog = |
| 273 | + { |
| 274 | + schema = firstSarif.schema |
| 275 | + version = firstSarif.version |
| 276 | + runs = |
| 277 | + [ { tool = |
| 278 | + { firstRun.tool with |
| 279 | + driver = |
| 280 | + { firstRun.tool.driver with |
| 281 | + rules = Some(List.ofSeq rules) } } |
| 282 | + invocations = firstRun.invocations |
| 283 | + columnKind = firstRun.columnKind |
| 284 | + results = List.ofSeq results } ] } |
| 285 | + |
| 286 | + sarifFiles |> Array.iter (fun (path, _) -> File.Delete(path)) |
| 287 | + |
| 288 | + let mergedStream = File.OpenWrite("analysisreports/merged.sarif") |
| 289 | + let combinedJson = writeSarif combined |
| 290 | + do! mergedStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes(combinedJson)) |
| 291 | + do! mergedStream.FlushAsync() |
| 292 | + mergedStream.Close() |
| 293 | + printfn $"Successfully merged %d{sarifFiles.Length} SARIF files" |
| 294 | + } |
0 commit comments