package sarif import ( "fmt" "github.com/google/uuid" "runtime/debug" "strconv" "strings" "github.com/securego/gosec/v2" "github.com/securego/gosec/v2/cwe" "github.com/securego/gosec/v2/report/core" ) type sarifLevel string const ( sarifNone = sarifLevel("none") sarifNote = sarifLevel("note") sarifWarning = sarifLevel("warning") sarifError = sarifLevel("error") cweAcronym = "CWE" ) //GenerateReport Convert a gosec report to a Sarif Report func GenerateReport(rootPaths []string, data *core.ReportInfo) (*Report, error) { type rule struct { index int rule *ReportingDescriptor } rules := make([]*ReportingDescriptor, 0) rulesIndices := make(map[string]rule) lastRuleIndex := -1 results := []*Result{} taxa := 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 taxon := parseSarifTaxon(weakness) taxa = append(taxa, taxon) } 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 := buildSarifResult(r.rule.ID, r.index, issue, []*Location{location}) results = append(results, result) } tool := buildSarifTool(buildSarifDriver(rules)) run := buildSarifRun(results, buildSarifTaxonomies(taxa), tool) return buildSarifReport(run), nil } func buildSarifResult(ruleID string, index int, issue *gosec.Issue, locations []*Location) *Result { return &Result{ RuleID: ruleID, RuleIndex: index, Level: getSarifLevel(issue.Severity.String()), Message: &Message{ Text: issue.What, }, Locations: locations, } } // buildSarifReport return SARIF report struct func buildSarifReport(run *Run) *Report { return &Report{ Version: "2.1.0", Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", Runs: []*Run{run}, } } // parseSarifRule return SARIF rule field struct func parseSarifRule(issue *gosec.Issue) *ReportingDescriptor { return &ReportingDescriptor{ ID: issue.RuleID, Name: issue.What, ShortDescription: &MultiformatMessageString{ Text: issue.What, }, FullDescription: &MultiformatMessageString{ Text: issue.What, }, Help: &MultiformatMessageString{ Text: fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\n", issue.What, issue.Severity.String(), issue.Confidence.String()), }, Properties: &PropertyBag{ Tags: []string{"security", issue.Severity.String()}, AdditionalProperties: map[string]interface{}{ "precision": strings.ToLower(issue.Confidence.String()), }, }, DefaultConfiguration: &ReportingConfiguration{ Level: getSarifLevel(issue.Severity.String()), }, Relationships: []*ReportingDescriptorRelationship{ buildSarifReportingDescriptorRelationship(issue.Cwe.ID), }, } } func buildSarifReportingDescriptorRelationship(weaknessID string) *ReportingDescriptorRelationship { return &ReportingDescriptorRelationship{ Target: &ReportingDescriptorReference{ ID: weaknessID, GUID: uuid3(cweAcronym + weaknessID), ToolComponent: &ToolComponentReference{ Name: cweAcronym, }, }, Kinds: []string{"superset"}, } } func buildSarifTool(driver *ToolComponent) *Tool { return &Tool{ Driver: driver, } } func buildSarifTaxonomies(taxa []*ReportingDescriptor) []*ToolComponent { return []*ToolComponent{ buildCWETaxonomy("4.4", taxa), } } func buildCWETaxonomy(version string, taxa []*ReportingDescriptor) *ToolComponent { return &ToolComponent{ Name: cweAcronym, Version: version, ReleaseDateUtc: "2021-03-15", InformationURI: fmt.Sprintf("https://cwe.mitre.org/data/published/cwe_v%s.pdf/", version), DownloadURI: fmt.Sprintf("https://cwe.mitre.org/data/xml/cwec_v%s.xml.zip", version), Organization: "MITRE", ShortDescription: &MultiformatMessageString{ Text: "The MITRE Common Weakness Enumeration", }, GUID: uuid3(cweAcronym), IsComprehensive: true, MinimumRequiredLocalizedDataSemanticVersion: version, Taxa: taxa, } } func parseSarifTaxon(weakness cwe.Weakness) *ReportingDescriptor { return &ReportingDescriptor{ ID: weakness.ID, Name: weakness.Name, GUID: uuid3(cweAcronym + weakness.ID), HelpURI: weakness.URL(), ShortDescription: &MultiformatMessageString{ Text: weakness.Description, }, } } func buildSarifDriver(rules []*ReportingDescriptor) *ToolComponent { buildInfo, ok := debug.ReadBuildInfo() var gosecVersion string if ok { gosecVersion = buildInfo.Main.Version[1:] } else { gosecVersion = "devel" } return &ToolComponent{ Name: "gosec", Version: gosecVersion, SupportedTaxonomies: []*ToolComponentReference{ {Name: cweAcronym, GUID: uuid3(cweAcronym)}, }, InformationURI: "https://github.com/securego/gosec/", Rules: rules, } } func uuid3(value string) string { return uuid.NewMD5(uuid.Nil, []byte(value)).String() } func buildSarifRun(results []*Result, taxonomies []*ToolComponent, tool *Tool) *Run { return &Run{ Results: results, Taxonomies: taxonomies, Tool: tool, } } // 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 buildSarifLocation(buildSarifPhysicalLocation(artifactLocation, region)), nil } func buildSarifLocation(physicalLocation *PhysicalLocation) *Location { return &Location{ PhysicalLocation: physicalLocation, } } func buildSarifPhysicalLocation(artifactLocation *ArtifactLocation, region *Region) *PhysicalLocation { return &PhysicalLocation{ ArtifactLocation: artifactLocation, Region: region, } } 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 buildSarifArtifactLocation(filePath) } func buildSarifArtifactLocation(uri string) *ArtifactLocation { return &ArtifactLocation{ URI: uri, } } 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 } return buildSarifRegion(startLine, endLine, col), nil } func buildSarifRegion(startLine int, endLine int, col int) *Region { return &Region{ StartLine: startLine, EndLine: endLine, StartColumn: col, EndColumn: col, SourceLanguage: "go", } } // From https://docs.oasis-open.org/sarif/sarif/v2.0/csprd02/sarif-v2.0-csprd02.html#_Toc10127839 // * "warning": The rule specified by ruleId was evaluated and a problem was found. // * "error": The rule specified by ruleId was evaluated and a serious problem was found. // * "note": The rule specified by ruleId was evaluated and a minor problem or an opportunity to improve the code was found. // * "none": The concept of “severity” does not apply to this result because the kind property (§3.27.9) has a value other than "fail". func getSarifLevel(s string) sarifLevel { switch s { case "LOW": return sarifWarning case "MEDIUM": return sarifError case "HIGH": return sarifError default: return sarifNote } }