gosec/report/sarif/formatter.go
2021-06-13 13:30:16 +02:00

202 lines
5.6 KiB
Go

package sarif
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/google/uuid"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/cwe"
)
// GenerateReport Convert a gosec report to a Sarif Report
func GenerateReport(rootPaths []string, data *gosec.ReportInfo) (*Report, error) {
type rule struct {
index int
rule *ReportingDescriptor
}
rules := make([]*ReportingDescriptor, 0)
rulesIndices := make(map[string]rule)
lastRuleIndex := -1
results := []*Result{}
cweTaxa := make([]*ReportingDescriptor, 0)
weaknesses := make(map[string]*cwe.Weakness)
for _, issue := range data.Issues {
_, ok := weaknesses[issue.Cwe.ID]
if !ok {
weakness := cwe.Get(issue.Cwe.ID)
weaknesses[issue.Cwe.ID] = weakness
cweTaxon := parseSarifTaxon(weakness)
cweTaxa = append(cweTaxa, cweTaxon)
}
r, ok := rulesIndices[issue.RuleID]
if !ok {
lastRuleIndex++
r = rule{index: lastRuleIndex, rule: parseSarifRule(issue)}
rulesIndices[issue.RuleID] = r
rules = append(rules, r.rule)
}
location, err := parseSarifLocation(issue, rootPaths)
if err != nil {
return nil, err
}
result := NewResult(r.rule.ID, r.index, getSarifLevel(issue.Severity.String()), issue.What).
WithLocations(location)
results = append(results, result)
}
sort.SliceStable(rules, func(i, j int) bool { return rules[i].ID < rules[j].ID })
sort.SliceStable(cweTaxa, func(i, j int) bool { return cweTaxa[i].ID < cweTaxa[j].ID })
tool := NewTool(buildSarifDriver(rules, data.GosecVersion))
cweTaxonomy := buildCWETaxonomy(cweTaxa)
run := NewRun(tool).
WithTaxonomies(cweTaxonomy).
WithResults(results...)
return NewReport(Version, Schema).
WithRuns(run), nil
}
// parseSarifRule return SARIF rule field struct
func parseSarifRule(issue *gosec.Issue) *ReportingDescriptor {
return &ReportingDescriptor{
ID: issue.RuleID,
Name: issue.What,
ShortDescription: NewMultiformatMessageString(issue.What),
FullDescription: NewMultiformatMessageString(issue.What),
Help: NewMultiformatMessageString(fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\n",
issue.What, issue.Severity.String(), issue.Confidence.String())),
Properties: &PropertyBag{
"tags": []string{"security", issue.Severity.String()},
"precision": strings.ToLower(issue.Confidence.String()),
},
DefaultConfiguration: &ReportingConfiguration{
Level: getSarifLevel(issue.Severity.String()),
},
Relationships: []*ReportingDescriptorRelationship{
buildSarifReportingDescriptorRelationship(issue.Cwe),
},
}
}
func buildSarifReportingDescriptorRelationship(weakness *cwe.Weakness) *ReportingDescriptorRelationship {
return &ReportingDescriptorRelationship{
Target: &ReportingDescriptorReference{
ID: weakness.ID,
GUID: uuid3(weakness.SprintID()),
ToolComponent: NewToolComponentReference(cwe.Acronym),
},
Kinds: []string{"superset"},
}
}
func buildCWETaxonomy(taxa []*ReportingDescriptor) *ToolComponent {
return NewToolComponent(cwe.Acronym, cwe.Version, cwe.InformationURI).
WithReleaseDateUtc(cwe.ReleaseDateUtc).
WithDownloadURI(cwe.DownloadURI).
WithOrganization(cwe.Organization).
WithShortDescription(NewMultiformatMessageString(cwe.Description)).
WithIsComprehensive(true).
WithLanguage("en").
WithMinimumRequiredLocalizedDataSemanticVersion(cwe.Version).
WithTaxa(taxa...)
}
func parseSarifTaxon(weakness *cwe.Weakness) *ReportingDescriptor {
return &ReportingDescriptor{
ID: weakness.ID,
GUID: uuid3(weakness.SprintID()),
HelpURI: weakness.SprintURL(),
FullDescription: NewMultiformatMessageString(weakness.Description),
ShortDescription: NewMultiformatMessageString(weakness.Name),
}
}
func parseSemanticVersion(version string) string {
if len(version) == 0 {
return "devel"
}
if strings.HasPrefix(version, "v") {
return version[1:]
}
return version
}
func buildSarifDriver(rules []*ReportingDescriptor, gosecVersion string) *ToolComponent {
semanticVersion := parseSemanticVersion(gosecVersion)
return NewToolComponent("gosec", gosecVersion, "https://github.com/securego/gosec/").
WithSemanticVersion(semanticVersion).
WithSupportedTaxonomies(NewToolComponentReference(cwe.Acronym)).
WithRules(rules...)
}
func uuid3(value string) string {
return uuid.NewMD5(uuid.Nil, []byte(value)).String()
}
// parseSarifLocation return SARIF location struct
func parseSarifLocation(issue *gosec.Issue, rootPaths []string) (*Location, error) {
region, err := parseSarifRegion(issue)
if err != nil {
return nil, err
}
artifactLocation := parseSarifArtifactLocation(issue, rootPaths)
return NewLocation(NewPhysicalLocation(artifactLocation, region)), nil
}
func parseSarifArtifactLocation(issue *gosec.Issue, rootPaths []string) *ArtifactLocation {
var filePath string
for _, rootPath := range rootPaths {
if strings.HasPrefix(issue.File, rootPath) {
filePath = strings.Replace(issue.File, rootPath+"/", "", 1)
}
}
return NewArtifactLocation(filePath)
}
func parseSarifRegion(issue *gosec.Issue) (*Region, error) {
lines := strings.Split(issue.Line, "-")
startLine, err := strconv.Atoi(lines[0])
if err != nil {
return nil, err
}
endLine := startLine
if len(lines) > 1 {
endLine, err = strconv.Atoi(lines[1])
if err != nil {
return nil, err
}
}
col, err := strconv.Atoi(issue.Col)
if err != nil {
return nil, err
}
snippet := NewArtifactContent(issue.Code)
return NewRegion(startLine, endLine, col, col, "go").WithSnippet(snippet), nil
}
func getSarifLevel(s string) Level {
switch s {
case "LOW":
return Warning
case "MEDIUM":
return Error
case "HIGH":
return Error
default:
return Note
}
}