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