package sarif import ( "fmt" "math" "os" "path" "sync" "github.com/pkg/errors" "github.com/projectdiscovery/nuclei/v3/pkg/catalog/config" "github.com/projectdiscovery/nuclei/v3/pkg/output" "github.com/projectdiscovery/sarif" ) // Exporter is an exporter for nuclei sarif output format. type Exporter struct { sarif *sarif.Report mutex *sync.Mutex rulemap map[string]*int // contains rule-id && ruleIndex rules []sarif.ReportingDescriptor options *Options } // Options contains the configuration options for sarif exporter client type Options struct { // File is the file to export found sarif result to File string `yaml:"file"` } // New creates a new sarif exporter integration client based on options. func New(options *Options) (*Exporter, error) { report := sarif.NewReport() exporter := &Exporter{ sarif: report, mutex: &sync.Mutex{}, rules: []sarif.ReportingDescriptor{}, rulemap: map[string]*int{}, options: options, } return exporter, nil } // addToolDetails adds details of static analysis tool (i.e nuclei) func (exporter *Exporter) addToolDetails() { driver := sarif.ToolComponent{ Name: "Nuclei", Organization: "ProjectDiscovery", Product: "Nuclei", ShortDescription: &sarif.MultiformatMessageString{ Text: "Fast and Customizable Vulnerability Scanner", }, FullDescription: &sarif.MultiformatMessageString{ Text: "Fast and customizable vulnerability scanner based on simple YAML based DSL", }, FullName: "Nuclei " + config.Version, SemanticVersion: config.Version, DownloadURI: "https://github.com/projectdiscovery/nuclei/releases", Rules: exporter.rules, } exporter.sarif.RegisterTool(driver) reportLocation := sarif.ArtifactLocation{ Uri: "file:///" + exporter.options.File, Description: &sarif.Message{ Text: "Nuclei Sarif Report", }, } invocation := sarif.Invocation{ CommandLine: os.Args[0], Arguments: os.Args[1:], ResponseFiles: []sarif.ArtifactLocation{reportLocation}, } exporter.sarif.RegisterToolInvocation(invocation) } // getSeverity in terms of sarif func (exporter *Exporter) getSeverity(severity string) (sarif.Level, string) { switch severity { case "critical": return sarif.Error, "9.4" case "high": return sarif.Error, "8" case "medium": return sarif.Note, "5" case "low": return sarif.Note, "2" case "info": return sarif.None, "1" } return sarif.None, "9.5" } // Export exports a passed result event to sarif structure func (exporter *Exporter) Export(event *output.ResultEvent) error { exporter.mutex.Lock() defer exporter.mutex.Unlock() severity := event.Info.SeverityHolder.Severity.String() resultHeader := fmt.Sprintf("%v (%v) found on %v", event.Info.Name, event.TemplateID, event.Host) resultLevel, vulnRating := exporter.getSeverity(severity) // Extra metadata if generated sarif is uploaded to GitHub security page ghMeta := map[string]interface{}{} ghMeta["tags"] = []string{"security"} ghMeta["security-severity"] = vulnRating // rule contain details of template rule := sarif.ReportingDescriptor{ Id: event.TemplateID, Name: event.Info.Name, FullDescription: &sarif.MultiformatMessageString{ // Points to template URL Text: event.Info.Description + "\nMore details at\n" + event.TemplateURL + "\n", }, Properties: ghMeta, } // GitHub Uses ShortDescription as title if event.Info.Description != "" { rule.ShortDescription = &sarif.MultiformatMessageString{ Text: resultHeader, } } // If rule is added ruleIndex := int(math.Max(0, float64(len(exporter.rules)-1))) if exporter.rulemap[rule.Id] == nil { exporter.rulemap[rule.Id] = &ruleIndex exporter.rules = append(exporter.rules, rule) } else { ruleIndex = *exporter.rulemap[rule.Id] } // vulnerability target/location location := sarif.Location{ Message: &sarif.Message{ Text: path.Join(event.Host, event.Path), }, PhysicalLocation: sarif.PhysicalLocation{ ArtifactLocation: sarif.ArtifactLocation{ // GitHub only accepts file:// protocol and local & relative files only // to avoid errors // is used which also translates to file according to specification Uri: "/" + event.Path, Description: &sarif.Message{ Text: path.Join(event.Host, event.Path), }, }, }, } // vulnerability report/result result := &sarif.Result{ RuleId: rule.Id, RuleIndex: ruleIndex, Level: resultLevel, Kind: sarif.Open, Message: &sarif.Message{ Text: resultHeader, }, Locations: []sarif.Location{location}, Rule: sarif.ReportingDescriptorReference{ Id: rule.Id, }, } exporter.sarif.RegisterResult(*result) return nil } // Close Writes data and closes the exporter after operation func (exporter *Exporter) Close() error { exporter.mutex.Lock() defer exporter.mutex.Unlock() if len(exporter.rules) == 0 { // no output if there are no results return nil } // links results and rules/templates exporter.addToolDetails() bin, err := exporter.sarif.Export() if err != nil { return errors.Wrap(err, "failed to generate sarif report") } if err := os.WriteFile(exporter.options.File, bin, 0644); err != nil { return errors.Wrap(err, "failed to create sarif file") } return nil }