github.com/bgpat/reviewdog@v0.0.0-20230909064023-077e44ca1f66/parser/sarif.go (about) 1 package parser 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "net/url" 8 "os" 9 "path/filepath" 10 11 "github.com/bgpat/reviewdog/proto/rdf" 12 "github.com/bgpat/reviewdog/service/serviceutil" 13 ) 14 15 var _ Parser = &SarifParser{} 16 17 // SarifParser is sarif parser. 18 type SarifParser struct{} 19 20 // NewSarifParser returns a new SarifParser. 21 func NewSarifParser() Parser { 22 return &SarifParser{} 23 } 24 25 func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) { 26 slf := new(SarifJson) 27 if err := json.NewDecoder(r).Decode(slf); err != nil { 28 return nil, err 29 } 30 var ds []*rdf.Diagnostic 31 basedir, err := os.Getwd() 32 if err != nil { 33 return nil, err 34 } 35 if root, err := serviceutil.GetGitRoot(); err == nil { 36 basedir = root 37 } 38 for _, run := range slf.Runs { 39 tool := run.Tool 40 driver := tool.Driver 41 name := driver.Name 42 informationURI := driver.InformationURI 43 baseURIs := run.OriginalURIBaseIds 44 rules := map[string]SarifRule{} 45 for _, rule := range driver.Rules { 46 rules[rule.ID] = rule 47 } 48 for _, result := range run.Results { 49 message := result.Message.GetText() 50 ruleID := result.RuleID 51 rule := rules[ruleID] 52 level := result.Level 53 if level == "" { 54 level = rule.DefaultConfiguration.Level 55 } 56 suggestionsMap := map[string][]*rdf.Suggestion{} 57 for _, fix := range result.Fixes { 58 for _, artifactChange := range fix.ArtifactChanges { 59 suggestions := []*rdf.Suggestion{} 60 path, err := artifactChange.ArtifactLocation.GetPath(baseURIs, basedir) 61 if err != nil { 62 // invalid path 63 return nil, err 64 } 65 for _, replacement := range artifactChange.Replacements { 66 deletedRegion := replacement.DeletedRegion 67 rng := deletedRegion.GetRdfRange() 68 if rng == nil { 69 // No line information in fix 70 continue 71 } 72 s := &rdf.Suggestion{ 73 Range: rng, 74 Text: replacement.InsertedContent.Text, 75 } 76 suggestions = append(suggestions, s) 77 } 78 suggestionsMap[path] = suggestions 79 } 80 } 81 for _, location := range result.Locations { 82 physicalLocation := location.PhysicalLocation 83 artifactLocation := physicalLocation.ArtifactLocation 84 path, err := artifactLocation.GetPath(baseURIs, basedir) 85 if err != nil { 86 // invalid path 87 return nil, err 88 } 89 region := physicalLocation.Region 90 rng := region.GetRdfRange() 91 if rng == nil { 92 // No line information in result 93 continue 94 } 95 var code *rdf.Code 96 if ruleID != "" { 97 code = &rdf.Code{ 98 Value: ruleID, 99 Url: rule.HelpURI, 100 } 101 } 102 d := &rdf.Diagnostic{ 103 Message: message, 104 Location: &rdf.Location{ 105 Path: path, 106 Range: rng, 107 }, 108 Severity: severity(level), 109 Source: &rdf.Source{ 110 Name: name, 111 Url: informationURI, 112 }, 113 Code: code, 114 Suggestions: suggestionsMap[path], 115 OriginalOutput: fmt.Sprintf( 116 "%v:%d:%d: %v: %v (%v)", 117 path, rng.Start.Line, getActualStartColumn(rng), level, message, ruleID, 118 ), 119 } 120 ds = append(ds, d) 121 } 122 } 123 } 124 return ds, nil 125 } 126 127 // SARIF JSON Format 128 // 129 // References: 130 // - https://sarifweb.azurewebsites.net/ 131 type SarifJson struct { 132 Runs []struct { 133 OriginalURIBaseIds map[string]SarifOriginalURI `json:"originalUriBaseIds"` 134 Results []struct { 135 Level string `json:"level"` 136 Locations []struct { 137 PhysicalLocation struct { 138 ArtifactLocation SarifArtifactLocation `json:"artifactLocation"` 139 Region SarifRegion `json:"region"` 140 } `json:"physicalLocation"` 141 } `json:"locations"` 142 Message SarifText `json:"message"` 143 RuleID string `json:"ruleId"` 144 Fixes []struct { 145 Description SarifText `json:"description"` 146 ArtifactChanges []struct { 147 ArtifactLocation SarifArtifactLocation `json:"artifactLocation"` 148 Replacements []struct { 149 DeletedRegion SarifRegion `json:"deletedRegion"` 150 InsertedContent struct { 151 Text string `json:"text"` 152 } `json:"insertedContent"` 153 } `json:"replacements"` 154 } `json:"artifactChanges"` 155 } `json:"fixes"` 156 } `json:"results"` 157 Tool struct { 158 Driver struct { 159 FullName string `json:"fullName"` 160 InformationURI string `json:"informationUri"` 161 Name string `json:"name"` 162 Rules []SarifRule `json:"rules"` 163 } `json:"driver"` 164 } `json:"tool"` 165 } `json:"runs"` 166 } 167 168 type SarifOriginalURI struct { 169 URI string `json:"uri"` 170 } 171 172 type SarifArtifactLocation struct { 173 URI string `json:"uri"` 174 URIBaseID string `json:"uriBaseId"` 175 Index int `json:"index"` 176 } 177 178 func (l *SarifArtifactLocation) GetPath( 179 baseURIs map[string]SarifOriginalURI, 180 basedir string, 181 ) (string, error) { 182 uri := l.URI 183 baseURI := baseURIs[l.URIBaseID].URI 184 if baseURI != "" { 185 if u, err := url.JoinPath(baseURI, uri); err == nil { 186 uri = u 187 } 188 } 189 parse, err := url.Parse(uri) 190 if err != nil { 191 return "", err 192 } 193 path := parse.Path 194 if relpath, err := filepath.Rel(basedir, path); err == nil { 195 path = relpath 196 } 197 return path, nil 198 } 199 200 type SarifText struct { 201 Text string `json:"text"` 202 Markdown *string `json:"markdown"` 203 } 204 205 func (t *SarifText) GetText() string { 206 text := t.Text 207 if t.Markdown != nil { 208 text = *t.Markdown 209 } 210 return text 211 } 212 213 type SarifRegion struct { 214 StartLine *int `json:"startLine"` 215 StartColumn *int `json:"startColumn"` 216 EndLine *int `json:"endLine"` 217 EndColumn *int `json:"endColumn"` 218 } 219 220 // convert SARIF Region to RDF Range 221 // 222 // * Supported SARIF: Line + Column Text region 223 // * Not supported SARIF: Offset + Length Text region ("charOffset", "charLength"), Binary region 224 // 225 // example text: 226 // 227 // abc\n 228 // def\n 229 // 230 // region: "abc" 231 // SARIF: { "startLine": 1 } 232 // 233 // = { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": null } 234 // 235 // -> RDF: { "start": { "line": 1 } } 236 // 237 // region: "bc" 238 // SARIF: { "startLine": 1, "startColumn": 2 } 239 // 240 // = { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": null } 241 // 242 // -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "line": 1 } } 243 // 244 // region: "a" 245 // SARIF: { "startLine": 1, "endColumn": 2 } 246 // 247 // = { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 2 } 248 // 249 // -> RDF: { "start": { "line": 1 }, "end": { "column": 2 } } 250 // 251 // = { "start": { "line": 1 }, "end": { "line": 1, "column": 2 } } 252 // 253 // region: "b" 254 // SARIF: { "startLine": 1, "startColumn": 2, "endColumn": 3 } 255 // 256 // = { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": 3 } 257 // 258 // -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "column": 3 } } 259 // 260 // = { "start": { "line": 1, "column": 2 }, "end": { "line": 1, column": 3 } } 261 // 262 // region: "abc\ndef" 263 // SARIF: { "startLine": 1, "endLine": 2 } 264 // 265 // = { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": null } 266 // 267 // -> RDF: { "start": { "line": 1 }, "end": { "line": 2 } } 268 // 269 // region: "abc\n" 270 // SARIF: { "startLine": 1, "endLine": 2, "endColumn": 1 } 271 // 272 // = { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": 1 } 273 // 274 // -> RDF: { "start": { "line": 1 }, "end": { "line": 2, "column": 1 } } 275 // 276 // zero width region: "{■}abc" 277 // SARIF: { "startLine": 1, "endColumn": 1 } 278 // 279 // = { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 } 280 // 281 // -> RDF: { "start": { "line": 1, "column": 1 } } 282 // 283 // = { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } } 284 func (r *SarifRegion) GetRdfRange() *rdf.Range { 285 if r.StartLine == nil { 286 // No line information 287 return nil 288 } 289 startLine := *r.StartLine 290 startColumn := 1 // default value of startColumn in SARIF is 1 291 if r.StartColumn != nil { 292 startColumn = *r.StartColumn 293 } 294 endLine := startLine // default value of endLine in SARIF is startLine 295 if r.EndLine != nil { 296 endLine = *r.EndLine 297 } 298 endColumn := 0 // default value of endColumn in SARIF is null (that means EOL) 299 if r.EndColumn != nil { 300 endColumn = *r.EndColumn 301 } 302 var end *rdf.Position 303 if startLine == endLine && startColumn == endColumn { 304 // zero width region 305 end = &rdf.Position{ 306 Line: int32(endLine), 307 Column: int32(endColumn), 308 } 309 } else { 310 // not zero width region 311 if startColumn == 1 { 312 // startColumn = 1 is default value, then omit it from result 313 startColumn = 0 314 } 315 if startLine != endLine { 316 // when multi line region, End property must be provided 317 end = &rdf.Position{ 318 Line: int32(endLine), 319 Column: int32(endColumn), 320 } 321 } else { 322 // when single line region 323 if startColumn == 0 && endColumn == 0 { 324 // if single whole line region, no End properties are needed 325 } else { 326 // otherwise, End property is needed 327 end = &rdf.Position{ 328 Line: int32(endLine), 329 Column: int32(endColumn), 330 } 331 } 332 } 333 } 334 rng := &rdf.Range{ 335 Start: &rdf.Position{ 336 Line: int32(startLine), 337 Column: int32(startColumn), 338 }, 339 End: end, 340 } 341 return rng 342 } 343 344 type SarifRule struct { 345 ID string `json:"id"` 346 Name string `json:"name"` 347 ShortDescription SarifText `json:"shortDescription"` 348 FullDescription SarifText `json:"fullDescription"` 349 Help SarifText `json:"help"` 350 HelpURI string `json:"helpUri"` 351 DefaultConfiguration struct { 352 Level string `json:"level"` 353 Rank int `json:"rank"` 354 } `json:"defaultConfiguration"` 355 } 356 357 func getActualStartColumn(r *rdf.Range) int32 { 358 startColumn := r.Start.Column 359 if startColumn == 0 { 360 // startColumn's default value means column 1 361 startColumn = 1 362 } 363 return startColumn 364 }