mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 19:25:26 +00:00
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.
197 lines
5.1 KiB
Go
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
|
|
}
|