Path: blob/dev/pkg/reporting/exporters/sarif/sarif.go
2072 views
package sarif12import (3"fmt"4"math"5"os"6"path"7"sync"89"github.com/pkg/errors"10"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"11"github.com/projectdiscovery/nuclei/v3/pkg/output"12"github.com/projectdiscovery/sarif"13)1415// Exporter is an exporter for nuclei sarif output format.16type Exporter struct {17sarif *sarif.Report18mutex *sync.Mutex19rulemap map[string]*int // contains rule-id && ruleIndex20rules []sarif.ReportingDescriptor21options *Options22}2324// Options contains the configuration options for sarif exporter client25type Options struct {26// File is the file to export found sarif result to27File string `yaml:"file"`28}2930// New creates a new sarif exporter integration client based on options.31func New(options *Options) (*Exporter, error) {32report := sarif.NewReport()33exporter := &Exporter{34sarif: report,35mutex: &sync.Mutex{},36rules: []sarif.ReportingDescriptor{},37rulemap: map[string]*int{},38options: options,39}40return exporter, nil41}4243// addToolDetails adds details of static analysis tool (i.e nuclei)44func (exporter *Exporter) addToolDetails() {45driver := sarif.ToolComponent{46Name: "Nuclei",47Organization: "ProjectDiscovery",48Product: "Nuclei",49ShortDescription: &sarif.MultiformatMessageString{50Text: "Fast and Customizable Vulnerability Scanner",51},52FullDescription: &sarif.MultiformatMessageString{53Text: "Fast and customizable vulnerability scanner based on simple YAML based DSL",54},55FullName: "Nuclei " + config.Version,56SemanticVersion: config.Version,57DownloadURI: "https://github.com/projectdiscovery/nuclei/releases",58Rules: exporter.rules,59}60exporter.sarif.RegisterTool(driver)6162reportLocation := sarif.ArtifactLocation{63Uri: "file:///" + exporter.options.File,64Description: &sarif.Message{65Text: "Nuclei Sarif Report",66},67}6869invocation := sarif.Invocation{70CommandLine: os.Args[0],71Arguments: os.Args[1:],72ResponseFiles: []sarif.ArtifactLocation{reportLocation},73}74exporter.sarif.RegisterToolInvocation(invocation)75}7677// getSeverity in terms of sarif78func (exporter *Exporter) getSeverity(severity string) (sarif.Level, string) {79switch severity {80case "critical":81return sarif.Error, "9.4"82case "high":83return sarif.Error, "8"84case "medium":85return sarif.Note, "5"86case "low":87return sarif.Note, "2"88case "info":89return sarif.None, "1"90}9192return sarif.None, "9.5"93}9495// Export exports a passed result event to sarif structure96func (exporter *Exporter) Export(event *output.ResultEvent) error {97exporter.mutex.Lock()98defer exporter.mutex.Unlock()99100severity := event.Info.SeverityHolder.Severity.String()101resultHeader := fmt.Sprintf("%v (%v) found on %v", event.Info.Name, event.TemplateID, event.Host)102resultLevel, vulnRating := exporter.getSeverity(severity)103104// Extra metadata if generated sarif is uploaded to GitHub security page105ghMeta := map[string]interface{}{}106ghMeta["tags"] = []string{"security"}107ghMeta["security-severity"] = vulnRating108109// rule contain details of template110rule := sarif.ReportingDescriptor{111Id: event.TemplateID,112Name: event.Info.Name,113FullDescription: &sarif.MultiformatMessageString{114// Points to template URL115Text: event.Info.Description + "\nMore details at\n" + event.TemplateURL + "\n",116},117Properties: ghMeta,118}119120// GitHub Uses ShortDescription as title121if event.Info.Description != "" {122rule.ShortDescription = &sarif.MultiformatMessageString{123Text: resultHeader,124}125}126127// If rule is added128ruleIndex := int(math.Max(0, float64(len(exporter.rules)-1)))129if exporter.rulemap[rule.Id] == nil {130exporter.rulemap[rule.Id] = &ruleIndex131exporter.rules = append(exporter.rules, rule)132} else {133ruleIndex = *exporter.rulemap[rule.Id]134}135136// vulnerability target/location137location := sarif.Location{138Message: &sarif.Message{139Text: path.Join(event.Host, event.Path),140},141PhysicalLocation: sarif.PhysicalLocation{142ArtifactLocation: sarif.ArtifactLocation{143// GitHub only accepts file:// protocol and local & relative files only144// to avoid errors // is used which also translates to file according to specification145Uri: "/" + event.Path,146Description: &sarif.Message{147Text: path.Join(event.Host, event.Path),148},149},150},151}152153// vulnerability report/result154result := &sarif.Result{155RuleId: rule.Id,156RuleIndex: ruleIndex,157Level: resultLevel,158Kind: sarif.Open,159Message: &sarif.Message{160Text: resultHeader,161},162Locations: []sarif.Location{location},163Rule: sarif.ReportingDescriptorReference{164Id: rule.Id,165},166}167168exporter.sarif.RegisterResult(*result)169170return nil171172}173174// Close Writes data and closes the exporter after operation175func (exporter *Exporter) Close() error {176exporter.mutex.Lock()177defer exporter.mutex.Unlock()178179if len(exporter.rules) == 0 {180// no output if there are no results181return nil182}183// links results and rules/templates184exporter.addToolDetails()185186bin, err := exporter.sarif.Export()187if err != nil {188return errors.Wrap(err, "failed to generate sarif report")189}190if err := os.WriteFile(exporter.options.File, bin, 0644); err != nil {191return errors.Wrap(err, "failed to create sarif file")192}193194return nil195}196197198