diff --git a/README.md b/README.md index 6a638b4..10ae2cb 100644 --- a/README.md +++ b/README.md @@ -81,42 +81,26 @@ For more information about SARIF, you can visit the [Oasis Open](https://www.oas ## Usage +Add an import to `go get github.com/owenrumney/go-sarif/sarif` ### Parsing a Sarif report -### +There are a number of ways to load in the content of a sarif report. +#### Open -Add an import to `github.com/owenrumney/go-sarif/sarif` +`sarif.Open` takes a file path and loads the sarif from that location. Returns a report and any corresponding error -Creating a new Sarif report is done by passing the version, the only supported at the moment is `2.1.0` - -The example below is taken from the `tfsec` usage of `go-sarif`. For context, at the end of the process a slice of `Result` objects is returned with the relevant information about the check failures. +#### FromBytes -```go -// create the report object -report, err := sarif.New(sarif.Version210) -if err != nil { - return err -} +`sarif.FromBytes` takes a slice of byte and returns a report and any corresponding error. -// add a run to the report -run := report.AddRun("tfsec", "https://tfsec.dev") +#### FromString -// for each result add the rule, location and result to the report -for _, result := range results { - rule := run.AddRule(string(result.RuleID)). - WithDescription(result.Description). - WithHelpUri(fmt.Sprintf("https://tfsec.dev/%s/%s", strings.ToLower(string(result.RuleProvider)), result.RuleID)) +`sarif.FromString` takes a string of the sarif content and returns a report and any corresponding error. - ruleResult := run.AddResult(rule.Id). - WithMessage(string(result.RuleDescription)). - WithLevel(string(result.Severity)). - WithLocationDetails(result.Range.Filename, result.Range.StartLine, 1) +### Creating a new report - run.AddResultDetails(rule, ruleResult, result.Range.Filename) -} +Creating a new Sarif report is done by passing the version, the only supported at the moment is `2.1.0` -// print the report to anything that implements `io.Writer` -return report.Write(w) -``` +for a detailed example check the example folder [example/main.go](example/main.go) diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..d942408 --- /dev/null +++ b/example/go.mod @@ -0,0 +1,5 @@ +module github.com/owenrumney/go-sarif/example + +go 1.16 + +require github.com/owenrumney/go-sarif v1.0.6 diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..e4a301f --- /dev/null +++ b/example/go.sum @@ -0,0 +1,31 @@ +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/owenrumney/go-sarif v1.0.6 h1:gmlbOaCf3Ya6afS67moMmz/P0F5N7SVgIvH6IZDi570= +github.com/owenrumney/go-sarif v1.0.6/go.mod h1:sgJM0ZaZ28jT8t8Iq3/mUCFBW9cX09EobIBXYOhiYBc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/zclconf/go-cty v1.8.3 h1:48gwZXrdSADU2UW9eZKHprxAI7APZGW9XmExpJpSjT0= +github.com/zclconf/go-cty v1.8.3/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..ef3e512 --- /dev/null +++ b/example/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "io/ioutil" + "os" + "strings" + + "github.com/owenrumney/go-sarif/sarif" +) + +// simple structure for the output of tfsec +type TfsecResults struct { + Results []struct { + RuleID string `json:"rule_id"` + RuleDescription string `json:"rule_description"` + RuleProvider string `json:"rule_provider"` + Link string `json:"link"` + Location struct { + Filename string `json:"filename"` + StartLine int `json:"start_line"` + EndLine int `json:"end_line"` + } `json:"location"` + Description string `json:"description"` + Impact string `json:"impact"` + Resolution string `json:"resolution"` + Severity string `json:"severity"` + Passed bool `json:"passed"` + } `json:"results"` +} + +func main() { + + // Get the results from the results file + tfsecResults, err := loadTfsecResults() + if err != nil { + panic(err) + } + + // create a new report object + report, err := sarif.New(sarif.Version210) + if err != nil { + panic(err) + } + + // create a run for tfsec + run := sarif.NewRun("tfsec", "https://tfsec.dev") + + // for each result, add the + for _, r := range tfsecResults.Results { + + // create a property bag for the non standard stuff + pb := sarif.NewPropertyBag() + pb.Add("impact", r.Impact) + pb.Add("resolution", r.Resolution) + + // create a new rule for each rule id + run.AddRule(r.RuleID). + WithDescription(r.Description). + WithHelp(r.Link). + WithProperties(pb.Properties) + + // add each of the results with the details of where the issue occurred + run.AddResult(r.RuleID). + WithLevel(strings.ToLower(r.Severity)). + WithMessage(sarif.NewTextMessage(r.Description)). + WithLocation( + sarif.NewLocationWithPhysicalLocation( + sarif.NewPhysicalLocation(). + WithArtifactLocation( + sarif.NewSimpleArtifactLocation(r.Location.Filename), + ).WithRegion( + sarif.NewSimpleRegion(r.Location.StartLine, r.Location.EndLine), + ), + ), + ) + } + + // add the run to the report + report.AddRun(run) + + // print the report to stdout + report.PrettyWrite(os.Stdout) + + // save the report + // report.WriteFile("example-report.sarif") + +} + +// load the example results file +func loadTfsecResults() (TfsecResults, error) { + + jsonResult, err := ioutil.ReadFile("results.json") + if err != nil { + panic(err) + } + + var results TfsecResults + + err = json.Unmarshal(jsonResult, &results) + return results, err +} diff --git a/example/results.json b/example/results.json new file mode 100644 index 0000000..11c736d --- /dev/null +++ b/example/results.json @@ -0,0 +1,148 @@ +{ + "results": [ + { + "rule_id": "AWS006", + "rule_description": "An ingress security group rule allows traffic from /0.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS006/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 4, + "end_line": 4 + }, + "description": "Resource 'aws_security_group_rule.my-rule' defines a fully open ingress security group rule.", + "impact": "Your port exposed to the internet", + "resolution": "Set a more restrictive cidr range", + "severity": "WARNING", + "passed": false + }, + { + "rule_id": "AZU003", + "rule_description": "Unencrypted managed disk.", + "rule_provider": "azure", + "link": "See https://tfsec.dev/docs/azure/AZU003/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 22, + "end_line": 22 + }, + "description": "Resource 'azurerm_managed_disk.source' defines an unencrypted managed disk.", + "impact": "", + "resolution": "", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS025", + "rule_description": "API Gateway domain name uses outdated SSL/TLS protocols.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS025/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 26, + "end_line": 27 + }, + "description": "Resource 'aws_api_gateway_domain_name.missing_security_policy' should include security_policy (defauls to outdated SSL/TLS policy).", + "impact": "Outdated SSL policies increase exposure to known vulnerabilites", + "resolution": "Use the most modern TLS/SSL policies available", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS025", + "rule_description": "API Gateway domain name uses outdated SSL/TLS protocols.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS025/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 30, + "end_line": 30 + }, + "description": "Resource 'aws_api_gateway_domain_name.empty_security_policy' defines outdated SSL/TLS policies (not using TLS_1_2).", + "impact": "Outdated SSL policies increase exposure to known vulnerabilites", + "resolution": "Use the most modern TLS/SSL policies available", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS025", + "rule_description": "API Gateway domain name uses outdated SSL/TLS protocols.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS025/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 34, + "end_line": 34 + }, + "description": "Resource 'aws_api_gateway_domain_name.outdated_security_policy' defines outdated SSL/TLS policies (not using TLS_1_2).", + "impact": "Outdated SSL policies increase exposure to known vulnerabilites", + "resolution": "Use the most modern TLS/SSL policies available", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS018", + "rule_description": "Missing description for security group/security group rule.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS018/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 2, + "end_line": 5 + }, + "description": "Resource 'aws_security_group_rule.my-rule' should include a description for auditing purposes.", + "impact": "Descriptions provide context for the firewall rule reasons", + "resolution": "Add descriptions for all security groups anf rules", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS004", + "rule_description": "Use of plain HTTP.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS004/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 9, + "end_line": 9 + }, + "description": "Resource 'aws_alb_listener.my-alb-listener' uses plain HTTP instead of HTTPS.", + "impact": "Your traffic is not protected", + "resolution": "Switch to HTTPS to benefit from TLS security features", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS003", + "rule_description": "AWS Classic resource usage.", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS003/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 12, + "end_line": 14 + }, + "description": "Resource 'aws_db_security_group.my-group' uses EC2 Classic. Use a VPC instead.", + "impact": "Classic resources are running in a shared environment with other customers", + "resolution": "Switch to VPC resources", + "severity": "ERROR", + "passed": false + }, + { + "rule_id": "AWS092", + "rule_description": "DynamoDB tables should use at rest encyption with a Customer Managed Key", + "rule_provider": "aws", + "link": "See https://tfsec.dev/docs/aws/AWS092/ for more information.", + "location": { + "filename": "/home/billybob/supertfsec/example/main.tf", + "start_line": 41, + "end_line": 56 + }, + "description": "Resource 'aws_dynamodb_table.bad_example' is not using KMS CMK for encryption", + "impact": "Using AWS managed keys does not allow for fine grained control", + "resolution": "Enable server side encrytion with a customer managed key", + "severity": "WARNING", + "passed": false + } + ] +} diff --git a/sarif/sarif.go b/sarif/sarif.go index 43bdcd9..78840d3 100644 --- a/sarif/sarif.go +++ b/sarif/sarif.go @@ -80,6 +80,15 @@ func getVersionSchema(version Version) (string, error) { return "", fmt.Errorf("version [%s] is not supported", version) } +// WriteFile will write the report to a file using a pretty formatter +func (sarif *Report) WriteFile(filename string) error { + file, err := os.OpenFile(filename, os.O_CREATE, 0744) + if err != nil { + return err + } + return sarif.PrettyWrite(file) +} + // Write writes the JSON as a string with no formatting func (sarif *Report) Write(w io.Writer) error { for _, run := range sarif.Runs {