Skip to content

Commit

Permalink
feat: SARIF Output Support (#192)
Browse files Browse the repository at this point in the history
* add sarif output support
  • Loading branch information
chtzvt authored May 2, 2023
1 parent 0d33e90 commit 468aeb2
Show file tree
Hide file tree
Showing 9 changed files with 3,672 additions and 10 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ Using the `--output-format (-f)` flag, legitify supports outputting the results

1. `human-readable` - Human-readable text (default).
2. `json` - Standard JSON.
3. `sarif` - SARIF format ([info](https://sarifweb.azurewebsites.net/)).

### Output Schemes

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ require (
github.com/olekukonko/tablewriter v0.0.5
github.com/open-policy-agent/opa v0.43.1
github.com/ossf/scorecard/v4 v4.4.0
github.com/owenrumney/go-sarif/v2 v2.1.3
github.com/qri-io/jsonschema v0.2.1
github.com/sashabaranov/go-gpt3 v1.1.0
github.com/shurcooL/githubv4 v0.0.0-20220520033151-0b4e3294ff00
github.com/spf13/cobra v1.5.0
Expand Down Expand Up @@ -73,6 +75,7 @@ require (
github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/qri-io/jsonpointer v0.1.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
github.com/rhysd/actionlint v1.6.13 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
Expand Down Expand Up @@ -1039,6 +1040,9 @@ github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/ossf/scorecard/v4 v4.4.0 h1:hQxfA3rfZhENVWBipBz0ED1aIoPiMyGJtwSCXOuMwoc=
github.com/ossf/scorecard/v4 v4.4.0/go.mod h1:ZdUMc/E6gz1GYGEUqdXj0qudvaeT1z1d78Py/zX2FZo=
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
github.com/owenrumney/go-sarif/v2 v2.1.3 h1:1guchw824yg1CwjredY8pnzcE0SG+sfNzFY5CUYWgE4=
github.com/owenrumney/go-sarif/v2 v2.1.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
Expand Down Expand Up @@ -1103,6 +1107,10 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA=
github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64=
github.com/qri-io/jsonschema v0.2.1 h1:NNFoKms+kut6ABPf6xiKNM5214jzxAhDBrPHCJ97Wg0=
github.com/qri-io/jsonschema v0.2.1/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rhysd/actionlint v1.6.13 h1:HAS71S4jLn3AGY7jbeLmTLH4NzHgOZWrZHuG3CqpCko=
Expand Down Expand Up @@ -1132,6 +1140,7 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
Expand Down Expand Up @@ -1237,6 +1246,8 @@ github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmF
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/xanzy/go-gitlab v0.76.0 h1:mkmuB27RDVZY/iXR61pEUfIqJ15Iivfu1kc3KZtBICI=
Expand All @@ -1262,6 +1273,7 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
Expand Down
224 changes: 224 additions & 0 deletions internal/outputer/formatter/formatter_sarif.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package formatter

import (
"encoding/json"
"fmt"
"strings"

"github.com/owenrumney/go-sarif/v2/sarif"

"github.com/Legit-Labs/legitify/internal/common/severity"
"github.com/Legit-Labs/legitify/internal/outputer/scheme"
)

type sarifFormatter struct {
colorizer sarifColorizer
}

func newSarifFormatter() OutputFormatter {
return &sarifFormatter{
colorizer: sarifColorizer{},
}
}

func (f *sarifFormatter) Format(s scheme.Scheme, failedOnly bool) ([]byte, error) {
report, err := sarif.New(sarif.Version210)
if err != nil {
return nil, err
}

typedOutput, ok := s.(*scheme.Flattened)
if !ok {
return nil, UnsupportedScheme{s}
}

run := sarif.NewRunWithInformationURI("legitify", "https://legitify.dev/")

for _, policyName := range s.AsOrderedMap().Keys() {
data := typedOutput.GetPolicyData(policyName)
policyInfo := data.PolicyInfo

pb := sarif.NewPropertyBag()
pb.Add("impact", policyInfo.Threat)
pb.Add("resolution", policyInfo.RemediationSteps)
pb.Add("precision", "high")
pb.Add("problem.severity", sarifProblemSeverity(policyInfo.Severity))
pb.Add("security-severity", sarifSecuritySeverity(policyInfo.Severity))

run.AddRule(policyInfo.FullyQualifiedPolicyName).
WithDescription(policyInfo.Description).
WithShortDescription(sarif.NewMultiformatMessageString(policyInfo.Title)).
WithProperties(pb.Properties).
WithTextHelp(getPlaintextPolicySummary(typedOutput, policyName)).
WithMarkdownHelp(getMarkdownPolicySummary(typedOutput, policyName))

// Tools like legitify don't fit perfectly into the SARIF model, so we're going to follow the
// lead of OpenSSF's scorecard output as a starting point.
// https://github.com/ossf/scorecard/blob/273dccda33590b7b46e98e19a9154f9da5400521/pkg/testdata/check6.sarif

for _, violation := range data.Violations {

var entityId interface{}
var ok bool

if violation.Aux != nil {
entityId, ok = violation.Aux.Get("entityId")
}

if !ok || violation.Aux == nil {
entityId = "unknown"
}

run.AddDistinctArtifact(violation.ViolationEntityType)
run.CreateResultForRule(policyInfo.FullyQualifiedPolicyName).
WithLevel(sarifSeverity(policyInfo.Severity)).
WithMessage(sarif.NewTextMessage(policyInfo.Description)).
WithHostedViewerUri(violation.CanonicalLink).
AddLocation(
sarif.NewLocationWithPhysicalLocation(
sarif.NewPhysicalLocation().
WithArtifactLocation(
sarif.NewArtifactLocation().
WithUri(fmt.Sprintf("%v", entityId)).
WithUriBaseId("legitify"),
),
),
)
}
}

report.AddRun(run)

bytes, err := json.MarshalIndent(report, "", DefaultOutputIndent)
if err != nil {
return nil, err
}
return bytes, nil
}

func (f *sarifFormatter) IsSchemeSupported(schemeType string) bool {
return true
}

// See https://github.com/github/docs/issues/21221
func sarifSeverity(s severity.Severity) string {
switch s {
case severity.Critical:
return "error"
case severity.High:
return "error"
case severity.Medium:
return "warning"
case severity.Low:
return "note"
default:
return "none"
}
}

func sarifProblemSeverity(s severity.Severity) string {
switch s {
case severity.Critical:
return "error"
case severity.High:
return "error"
case severity.Medium:
return "warning"
case severity.Low:
return "recommendation"
default:
return "recommendation"
}
}

func sarifSecuritySeverity(s severity.Severity) string {
switch s {
case severity.Critical:
return "9.0"
case severity.High:
return "7.0"
case severity.Medium:
return "4.0"
case severity.Low:
return "1.0"
default:
return "1.0"
}
}

func getPlaintextPolicySummary(output *scheme.Flattened, policyName string) string {
sFormatter := newSarifFormatter()
typedFormatter := sFormatter.(*sarifFormatter)
pf := newSarifPolicyFormatter()
pc := newPoliciesContent(pf, typedFormatter.colorizer)
return string(pc.FormatPolicy(output, policyName))
}

func getMarkdownPolicySummary(output *scheme.Flattened, policyName string) string {
mdFormatter := newMarkdownFormatter()
typedFormatter := mdFormatter.(*markdownFormatter)
pf := newMarkdownPolicyFormatter()
pc := newPoliciesContent(pf, typedFormatter.colorizer)
return string(pc.FormatPolicy(output, policyName))
}

type sarifColorizer struct {
}

func (sc sarifColorizer) colorize(tColor themeColor, text interface{}) string {
return text.(string)
}

// plaintext policy formatting
type sarifPolicyFormatter struct {
colorizer sarifColorizer
}

func newSarifPolicyFormatter() sarifPolicyFormatter {
return sarifPolicyFormatter{colorizer: sarifColorizer{}}
}

func (sp sarifPolicyFormatter) FormatTitle(title string, severity severity.Severity) string {
color := severityToThemeColor(severity)
title = sp.colorizer.colorize(color, title)

return title
}

func (sp sarifPolicyFormatter) FormatSubtitle(title string) string {
return title
}

func (sp sarifPolicyFormatter) FormatText(depth int, format string, args ...interface{}) string {
return indentMultilineSpecial(depth, fmt.Sprintf(format, args...), sp.Indent(1), sp.Linebreak())
}

func (sp sarifPolicyFormatter) FormatList(depth int, title string, list []string, ordered bool) string {
if len(list) == 0 {
return ""
}

var sb strings.Builder
bullet := "*"
sb.WriteString(sp.FormatText(depth, "%s\n", title))
for i, step := range list {
if ordered {
bullet = fmt.Sprintf("%d.", i+1)
}
sb.WriteString(sp.FormatText(depth, "%s %s\n", bullet, step))
}

return sb.String()
}

func (sp sarifPolicyFormatter) Linebreak() string {
return " \n"
}

func (sp sarifPolicyFormatter) Separator() string {
return "---"
}

func (sp sarifPolicyFormatter) Indent(depth int) string {
return strings.Repeat(" ", depth)
}
52 changes: 52 additions & 0 deletions internal/outputer/formatter/formatter_sarif_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package formatter_test

import (
"os"
"fmt"
"context"
"encoding/json"
"testing"

"github.com/Legit-Labs/legitify/internal/outputer/formatter"
"github.com/Legit-Labs/legitify/internal/outputer/scheme/scheme_test"
"github.com/qri-io/jsonschema"
"github.com/stretchr/testify/require"
)

func TestFormatSarif(t *testing.T) {
sample := scheme_test.SchemeSample()

for _, f := range []bool{true, false} {
bytes, err := formatter.Format(formatter.Sarif, formatter.DefaultOutputIndent, sample, f)
require.Nilf(t, err, "Error formatting sarif: %v", err)
require.NotNil(t, bytes, "Error formatting sarif")
require.NotEmpty(t, bytes, "Error formatting sarif")

ctx := context.Background()

schemaData, err := os.ReadFile("formatter_test/sarif_v2.1.0_schema.json")
if err != nil {
panic(err)
}

// QRI + JSON schema draft-07 compatibility
// See https://github.com/qri-io/jsonschema/issues/114#issuecomment-1102010496
jsonschema.RegisterKeyword("definitions", jsonschema.NewDefs)

rs := &jsonschema.Schema{}
if err := json.Unmarshal(schemaData, rs); err != nil {
panic("unmarshal schema: " + err.Error())
}

errs, err := rs.ValidateBytes(ctx, bytes)
if err != nil {
panic(err)
}

if len(errs) > 0 {
fmt.Println(errs[0].Error())
}

require.Emptyf(t, errs, "SARIF output does not match schema: %v", errs)
}
}
Loading

0 comments on commit 468aeb2

Please sign in to comment.