Skip to content

Commit cbee488

Browse files
authored
Merge pull request #3177 from nojaf/merge-sarif
Merge sarif files
2 parents 1b0e886 + c0f4e4b commit cbee488

File tree

4 files changed

+307
-2
lines changed

4 files changed

+307
-2
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
if: matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main'
2929
run: "curl -H 'Accept: application/vnd.github.everest-preview+json' -H 'Authorization: token ${{secrets.FANTOMAS_TOOLS_TOKEN}}' --request POST --data '{\"event_type\": \"fantomas-commit-on-main\"}' https://api.github.com/repos/fsprojects/fantomas-tools/dispatches"
3030
- name: "Run analyzers"
31-
run: dotnet msbuild /t:AnalyzeSolution
31+
run: dotnet fsi build.fsx -- -p Analyze
3232
continue-on-error: true
3333
if: matrix.os == 'ubuntu-latest'
3434
- name: Upload SARIF file

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,7 @@ tests/.repositories/**
199199

200200
# Analyzer files
201201
.analyzerpackages
202-
*.sarif
202+
*.sarif
203+
204+
# vscode history plugin
205+
.history/

build.fsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#r "nuget: FSharp.Data, 6.3.0"
44
#r "nuget: Ionide.KeepAChangelog, 0.1.8"
55
#r "nuget: Humanizer.Core, 2.14.1"
6+
#load "./sarif.fsx"
67

78
open System
89
open System.IO
@@ -499,4 +500,11 @@ pipeline "PublishAlpha" {
499500
runIfOnlySpecified true
500501
}
501502

503+
pipeline "Analyze" {
504+
workingDir __SOURCE_DIRECTORY__
505+
stage "Analyze" { run "dotnet msbuild /t:AnalyzeSolution" }
506+
stage "Merge" { run Sarif.mergeSarifFiles }
507+
runIfOnlySpecified true
508+
}
509+
502510
tryPrintPipelineCommandHelp ()

sarif.fsx

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
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

Comments
 (0)