David Fisher 143f179e7b
Remove prefix v from Sarif exporters (#4976)
because: In config.Version there is already
a `v`` prefix, such as `v3.2.2``.

Prior to this commit the versions were being
tagged as `vv3.2.2`

this commit: Removes the 'v' prefix from the
Sarif exporter in the ToolDetails for both
FullName and SemanticVersion.
2024-04-03 17:19:27 +05:30

197 lines
5.1 KiB
Go

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
}