package sarif import ( "bytes" "os" "path" "strings" "sync" "github.com/owenrumney/go-sarif/sarif" "github.com/pkg/errors" "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/format" "github.com/projectdiscovery/nuclei/v2/pkg/types" ) // Exporter is an exporter for nuclei sarif output format. type Exporter struct { sarif *sarif.Report run *sarif.Run mutex *sync.Mutex home string 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 disk exporter integration client based on options. func New(options *Options) (*Exporter, error) { report, err := sarif.New(sarif.Version210) if err != nil { return nil, errors.Wrap(err, "could not create sarif exporter") } home, err := os.UserHomeDir() if err != nil { return nil, errors.Wrap(err, "could not get home dir") } templatePath := path.Join(home, "nuclei-templates") run := sarif.NewRun("nuclei", "https://github.com/projectdiscovery/nuclei") return &Exporter{options: options, home: templatePath, sarif: report, run: run, mutex: &sync.Mutex{}}, nil } // Export exports a passed result event to sarif structure func (i *Exporter) Export(event *output.ResultEvent) error { templatePath := strings.TrimPrefix(event.TemplatePath, i.home) description := getSarifResultMessage(event, templatePath) sarifSeverity := getSarifSeverity(event) sarifRuleHelpURIs := getSarifRuleHelpURIFromReferences(event) var ruleName string if s, ok := event.Info["name"]; ok { ruleName = s.(string) } var templateURL string if strings.HasPrefix(event.TemplatePath, i.home) { templateURL = "https://github.com/projectdiscovery/nuclei-templates/blob/master" + templatePath } var ruleDescription string if d, ok := event.Info["description"]; ok { ruleDescription = d.(string) } builder := &strings.Builder{} builder.WriteString(ruleDescription) if sarifRuleHelpURIs != "" { builder.WriteString("\nReferences: \n") builder.WriteString(sarifRuleHelpURIs) } if templateURL != "" { builder.WriteString("\nTemplate URL: ") builder.WriteString(templateURL) } ruleHelp := builder.String() i.mutex.Lock() defer i.mutex.Unlock() _ = i.run.AddRule(event.TemplateID). WithDescription(ruleName). WithHelp(ruleHelp). WithHelpURI(templateURL). WithFullDescription(sarif.NewMultiformatMessageString(sarifRuleHelpURIs)) _ = i.run.AddResult(event.TemplateID). WithMessage(sarif.NewMessage().WithText(description)). WithLevel(sarifSeverity). WithLocation(sarif.NewLocation().WithMessage(sarif.NewMessage().WithText(event.Host)).WithPhysicalLocation( sarif.NewPhysicalLocation(). WithArtifactLocation(sarif.NewArtifactLocation().WithUri(event.Type)). WithRegion(sarif.NewRegion().WithStartColumn(1).WithStartLine(1).WithEndLine(1).WithEndColumn(1)), )) return nil } // getSarifSeverity returns the sarif severity func getSarifSeverity(event *output.ResultEvent) string { var ruleSeverity string if s, ok := event.Info["severity"]; ok { ruleSeverity = s.(string) } switch ruleSeverity { case "info": return "none" case "low", "medium": return "warning" case "high", "critical": return "error" default: return "none" } } // getSarifRuleHelpURIFromReferences returns the sarif rule help uri func getSarifRuleHelpURIFromReferences(event *output.ResultEvent) string { if d, ok := event.Info["reference"]; ok { switch v := d.(type) { case string: return v case []interface{}: slice := types.ToStringSlice(v) return strings.Join(slice, "\n") } } return "" } // getSarifResultMessage gets a sarif result message from event func getSarifResultMessage(event *output.ResultEvent, templatePath string) string { template := format.GetMatchedTemplate(event) builder := &bytes.Buffer{} builder.WriteString(template) builder.WriteString(" matched at ") builder.WriteString(event.Host) builder.WriteString(" (") builder.WriteString(strings.ToUpper(event.Type)) builder.WriteString(") => ") builder.WriteString(event.Matched) if len(event.ExtractedResults) > 0 || len(event.Metadata) > 0 { if len(event.ExtractedResults) > 0 { builder.WriteString(" **Extracted results**:

") for _, v := range event.ExtractedResults { builder.WriteString("- ") builder.WriteString(v) builder.WriteString("
") } builder.WriteString("
") } if len(event.Metadata) > 0 { builder.WriteString(" **Metadata**:
") for k, v := range event.Metadata { builder.WriteString("- ") builder.WriteString(k) builder.WriteString(": ") builder.WriteString(types.ToString(v)) builder.WriteString("
") } builder.WriteString("
") } } if event.Interaction != nil { builder.WriteString("**Interaction Data**\n---\n") builder.WriteString(event.Interaction.Protocol) } builder.WriteString("
To Reproduce - `nuclei -t ") builder.WriteString(strings.TrimPrefix(templatePath, "/")) builder.WriteString(" -target \"") builder.WriteString(event.Host) builder.WriteString("\"`") data := builder.String() return data } // Close closes the exporter after operation func (i *Exporter) Close() error { i.mutex.Lock() defer i.mutex.Unlock() i.sarif.AddRun(i.run) file, err := os.Create(i.options.File) if err != nil { return errors.Wrap(err, "could not create sarif output file") } defer file.Close() return i.sarif.Write(file) }