Skip to content

Commit

Permalink
feat: Add support for SARIF reporting #22
Browse files Browse the repository at this point in the history
  • Loading branch information
abhisek committed Jun 21, 2024
1 parent c2175fe commit 5ddfcfc
Show file tree
Hide file tree
Showing 11 changed files with 507 additions and 112 deletions.
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ CI/CD and `policy as code` as guardrails.
* [Scanning Package URL](#scanning-package-url)
* [Available Parsers](#available-parsers)
* [Policy as Code](#policy-as-code)
* [Query Mode](#query-mode)
* [Reporting](#reporting)
* [CI/CD Integration](#ci/cd-integration)
* [📦 GitHub Action](#-github-action)
* [🚀 GitLab CI](#-gitlab-ci)
Expand Down Expand Up @@ -187,6 +189,41 @@ vet scan -D /path/to/code \

For more examples, refer to [documentation](https://docs.safedep.io/advanced/polic-as-code)

## Query Mode

- Run scan and dump internal data structures to a file for further querying

```bash
vet scan -D /path/to/code --json-dump-dir /path/to/dump
```

- Filter results using `query` command

```bash
vet query --from /path/to/dump \
--filter 'vulns.critical.exists(p, true) || vulns.high.exists(p, true)'
```

- Generate report from dumped data

```bash
vet query --from /path/to/dump --report-json /path/to/report.json
```

## Reporting

`vet` supports generating reports in multiple formats during `scan` or `query`
execution.

| Format | Description |
|----------|--------------------------------------------------------------------------------|
| Markdown | Human readable report for vulnerabilities, licenses, and more |
| CSV | Export data to CSV format for manual slicing and dicing |
| JSON | Machine readable JSON format following internal schema (maximum data) |
| SARIF | Useful for integration with Github Code Scanning and other tools |
| Graph | Dependency graph in DOT format for risk and package relationship visualization |
| Summary | Default console report with summary of vulnerabilities, licenses, and more |

## CI/CD Integration

### 📦 GitHub Action
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/google/osv-scanner v1.7.4
github.com/jedib0t/go-pretty/v6 v6.5.9
github.com/kubescape/go-git-url v0.0.30
github.com/owenrumney/go-sarif/v2 v2.3.1
github.com/package-url/packageurl-go v0.1.3
github.com/safedep/dry v0.0.0-20240405050202-3b26d9386e57
github.com/sirupsen/logrus v1.9.3
Expand All @@ -28,6 +29,8 @@ require (
gopkg.in/yaml.v2 v2.4.0
)

replace github.com/owenrumney/go-sarif/v2 v2.3.1 => github.com/safedep/go-sarif/v2 v2.3.1

require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
Expand All @@ -44,8 +47,6 @@ require (
github.com/bytedance/sonic v1.11.8 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/chainguard-dev/git-urls v1.0.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
Expand Down Expand Up @@ -118,7 +119,6 @@ require (
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240610135401-a8a62080eff3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
Expand Down
117 changes: 18 additions & 99 deletions go.sum

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import (
)

var (
verbose bool
debug bool
noBanner bool
logFile string
globalExceptionsFile string
verbose bool
debug bool
noBanner bool
logFile string
globalExceptionsFile string
globalExceptionsExtra []string
)

var banner string = `
Expand Down Expand Up @@ -55,6 +56,7 @@ func main() {
cmd.PersistentFlags().BoolVarP(&noBanner, "no-banner", "", false, "Do not display the vet banner")
cmd.PersistentFlags().StringVarP(&logFile, "log", "l", "", "Write command logs to file, use - as for stdout")
cmd.PersistentFlags().StringVarP(&globalExceptionsFile, "exceptions", "e", "", "Load exceptions from file")
cmd.PersistentFlags().StringSliceVarP(&globalExceptionsExtra, "exceptions-extra", "", []string{}, "Load additional exceptions from file")

cmd.AddCommand(newAuthCommand())
cmd.AddCommand(newScanCommand())
Expand All @@ -74,18 +76,26 @@ func main() {
}

func loadExceptions() {
if globalExceptionsFile == "" {
loadExceptionsFromFile(globalExceptionsFile)

for _, extra := range globalExceptionsExtra {
loadExceptionsFromFile(extra)
}
}

func loadExceptionsFromFile(file string) {
if file == "" {
return
}

loader, err := exceptions.NewExceptionsFileLoader(globalExceptionsFile)
loader, err := exceptions.NewExceptionsFileLoader(file)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
logger.Fatalf("Failed to create Exceptions loader: %v", err)
}

err = exceptions.Load(loader)
if err != nil {
logger.Fatalf("Exceptions loader: %v", err)
logger.Fatalf("Failed to load exceptions: %v", err)
}
}

Expand Down
2 changes: 1 addition & 1 deletion pkg/exceptions/exceptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (s *exceptionStore) Match(pkg *models.Package) (*exceptionMatchResult, erro
return &result, nil
}

func (r *exceptionRule) matchByPattern(pkg *models.Package) bool {
func (r *exceptionRule) matchByPattern(_ *models.Package) bool {
return false
}

Expand Down
201 changes: 201 additions & 0 deletions pkg/reporter/sarif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
package reporter

import (
"fmt"
"os"
"strings"

"github.com/owenrumney/go-sarif/v2/sarif"
"github.com/safedep/dry/utils"
"github.com/safedep/vet/gen/checks"
"github.com/safedep/vet/pkg/analyzer"
"github.com/safedep/vet/pkg/common/logger"
"github.com/safedep/vet/pkg/models"
"github.com/safedep/vet/pkg/policy"
"github.com/safedep/vet/pkg/reporter/markdown"
)

// We will generate SARIF report for integration with
// different consumer tools. The design goal is to
// publish the following information in order of priority:
//
// 1. Policy violations
// 2. Package vulnerabilities
//
// We will not publish all package information. JSON
// report should be used for that purpose.

type SarifToolMetadata struct {
Name string
Version string
}

type SarifReporterConfig struct {
Tool SarifToolMetadata
Path string
}

type sarifReporter struct {
config SarifReporterConfig
report *sarif.Report
run *sarif.Run
rulesCache map[string]bool
violationsCache map[string]bool
}

func NewSarifReporter(config SarifReporterConfig) (Reporter, error) {
report, err := sarif.New(sarif.Version210)
if err != nil {
return nil, err
}

run := sarif.NewRunWithInformationURI(config.Tool.Name,
"https://github.com/safedep/vet")

run.Tool.Driver.Version = &config.Tool.Version
run.Tool.Driver.Properties = sarif.Properties{
"name": config.Tool.Name,
"version": config.Tool.Version,
}

return &sarifReporter{
config: config,
report: report,
run: run,
rulesCache: make(map[string]bool),
violationsCache: make(map[string]bool),
}, nil
}

func (r *sarifReporter) Name() string {
return "sarif"
}

func (r *sarifReporter) AddManifest(manifest *models.PackageManifest) {
a := sarif.NewArtifact().
WithLocation(sarif.NewSimpleArtifactLocation(manifest.GetDisplayPath()))
r.run.Artifacts = append(r.run.Artifacts, a)
}

func (r *sarifReporter) AddAnalyzerEvent(event *analyzer.AnalyzerEvent) {
r.recordFilterMatchEvent(event)
r.recordThreatEvent(event)
}

func (r *sarifReporter) AddPolicyEvent(event *policy.PolicyEvent) {
}

func (r *sarifReporter) Finish() error {
logger.Infof("Writing SARIF report to %s", r.config.Path)

fd, err := os.OpenFile(r.config.Path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}

defer fd.Close()

r.report.AddRun(r.run)
return r.report.Write(fd)
}

func (r *sarifReporter) recordThreatEvent(event *analyzer.AnalyzerEvent) {
if !event.IsLockfilePoisoningSignal() {
return
}

// TODO: Handle threat events in a generic way
}

func (r *sarifReporter) recordFilterMatchEvent(event *analyzer.AnalyzerEvent) {
if !event.IsFilterMatch() {
return
}

if (event.Package == nil) || (event.Package.Manifest == nil) || (event.Filter == nil) {
logger.Warnf("SARIF: Invalid event: missing package or manifest or filter")
return
}

if _, ok := r.rulesCache[event.Filter.GetName()]; !ok {
rule := sarif.NewRule(event.Filter.GetName())
rule.ShortDescription = sarif.NewMultiformatMessageString(event.Filter.GetSummary())
rule.Properties = sarif.Properties{
"filter": event.Filter.GetValue(),
"type": event.Filter.GetCheckType(),
}

r.run.Tool.Driver.Rules = append(r.run.Tool.Driver.Rules, rule)
r.rulesCache[event.Filter.GetName()] = true
}

uniqueInstance := fmt.Sprintf("%s/%s/%s",
event.Package.GetName(), event.Manifest.GetDisplayPath(), event.Filter.GetName())
if _, ok := r.violationsCache[uniqueInstance]; ok {
return
}

result := sarif.NewRuleResult(event.Filter.GetName())

result.WithLevel("error")
result.WithMessage(r.buildFilterResultMessageMarkdown(event))

pLocation := sarif.NewPhysicalLocation().
WithArtifactLocation(sarif.NewSimpleArtifactLocation(event.Manifest.GetDisplayPath()))
result.Locations = append(result.Locations, sarif.NewLocation().WithPhysicalLocation(pLocation))

r.run.AddResult(result)
}

func (r *sarifReporter) buildFilterResultMessageMarkdown(event *analyzer.AnalyzerEvent) *sarif.Message {
md := markdown.NewMarkdownBuilder()

md.AddHeader(2, "Policy Violation")
md.AddParagraph(fmt.Sprintf("Package `%s` violates policy `%s`.",
event.Package.GetName(), event.Filter.GetName()))

insights := utils.SafelyGetValue(event.Package.Insights)

if event.Filter.GetCheckType() == checks.CheckType_CheckTypeVulnerability {
md.AddHeader(3, "Vulnerabilities")

vulns := utils.SafelyGetValue(insights.Vulnerabilities)
for _, vuln := range vulns {
vid := utils.SafelyGetValue(vuln.Id)
md.AddBulletPoint(fmt.Sprintf("[%s](%s): %s",
vid,
vulnIdToLink(vid),
utils.SafelyGetValue(vuln.Summary)))
}

} else if event.Filter.GetCheckType() == checks.CheckType_CheckTypeLicense {
md.AddHeader(3, "Licenses")

licenses := utils.SafelyGetValue(insights.Licenses)
for _, license := range licenses {
md.AddBulletPoint(string(license))
}
} else if event.Filter.GetCheckType() == checks.CheckType_CheckTypePopularity {
projects := utils.SafelyGetValue(insights.Projects)

if len(projects) > 0 {
projectSource := utils.SafelyGetValue(projects[0].Type)

if strings.ToLower(projectSource) == "github" {
md.AddHeader(3, "GitHub Project")
md.AddBulletPoint(fmt.Sprintf("Name: %s", utils.SafelyGetValue(projects[0].Name)))
md.AddBulletPoint(fmt.Sprintf("Stars: %d", utils.SafelyGetValue(projects[0].Stars)))
md.AddBulletPoint(fmt.Sprintf("Forks: %d", utils.SafelyGetValue(projects[0].Forks)))
md.AddBulletPoint(fmt.Sprintf("Issues: %d", utils.SafelyGetValue(projects[0].Issues)))
md.AddBulletPoint(fmt.Sprintf("URL: %s", utils.SafelyGetValue(projects[0].Link)))
}
}
}

// SARIF spec mandates that we provide text in addition to markdown
msg := sarif.NewMessage().
WithMarkdown(md.Build()).
WithText(md.Build())

return msg
}
Loading

0 comments on commit 5ddfcfc

Please sign in to comment.