Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SARIF Output Support #192

Merged
merged 11 commits into from
May 2, 2023
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)
}
gal-legit marked this conversation as resolved.
Show resolved Hide resolved
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