Repository: datreeio/datree
Pull request #832 (2022-11-08 09:34:15)

feat: add sarif output


Patch:
diff --git a/go.mod b/go.mod
index b99d8af..c0e27fd 100644
--- a/go.mod
+++ b/go.mod
@@ -73,6 +73,7 @@ require (
 require (
 	github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
 	github.com/mikefarah/yq/v4 v4.27.3
+	github.com/owenrumney/go-sarif/v2 v2.1.2
 	github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
 	github.com/shirou/gopsutil/v3 v3.22.2
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
diff --git a/go.sum b/go.sum
index a535d09..8ad83e0 100644
--- a/go.sum
+++ b/go.sum
@@ -52,6 +52,7 @@ github.com/alecthomas/participle/v2 v2.0.0-beta.5/go.mod h1:RC764t6n4L8D8ITAJv0q
 github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+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/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -321,6 +322,10 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
+github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
+github.com/owenrumney/go-sarif/v2 v2.1.2 h1:PMDK7tXShJ9zsB7bfvlpADH5NEw1dfA9xwU8Xtdj73U=
+github.com/owenrumney/go-sarif/v2 v2.1.2/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 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -397,6 +402,8 @@ github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ
 github.com/tklauser/numcpus v0.3.0 h1:ILuRUQBtssgnxw0XXIjKUC56fgnOrFoQQ/4+DeU2biQ=
 github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+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/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -414,6 +421,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
 github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
diff --git a/pkg/evaluation/output_flag.go b/pkg/evaluation/output_flag.go
index 8303499..db781f1 100644
--- a/pkg/evaluation/output_flag.go
+++ b/pkg/evaluation/output_flag.go
@@ -2,10 +2,10 @@ package evaluation
 
 import "strings"
 
-var FormattedOutputOptions = []string{"yaml", "json", "xml", "JUnit"}
+var FormattedOutputOptions = []string{"yaml", "json", "xml", "JUnit", "sarif"}
 var InteractiveOutputOptions = []string{"", "simple"}
 var ValidOutputOptions = append(FormattedOutputOptions, InteractiveOutputOptions...)
-var ExplicitOutputOptions = []string{"simple", "yaml", "json", "xml", "JUnit"}
+var ExplicitOutputOptions = []string{"simple", "yaml", "json", "xml", "JUnit", "sarif"}
 
 func IsValidOutputOption(option string) bool {
 	for _, validOption := range ValidOutputOptions {
diff --git a/pkg/evaluation/printer.go b/pkg/evaluation/printer.go
index cd87106..1368b1f 100644
--- a/pkg/evaluation/printer.go
+++ b/pkg/evaluation/printer.go
@@ -13,6 +13,7 @@ import (
 	"github.com/datreeio/datree/bl/validation"
 	"github.com/datreeio/datree/pkg/extractor"
 	"github.com/fatih/color"
+	"github.com/owenrumney/go-sarif/v2/sarif"
 
 	"github.com/datreeio/datree/pkg/printer"
 	"gopkg.in/yaml.v2"
@@ -39,6 +40,7 @@ type PrintResultsData struct {
 	Verbose               bool
 	PolicyName            string
 	K8sValidationWarnings validation.K8sValidationWarningPerValidFile
+	CliVersion            string
 	IsCI                  bool
 }
 
@@ -133,6 +135,8 @@ func GetResultsText(resultsData *PrintResultsData) (string, error) {
 			return getXmlOutput(&formattedOutput)
 		case "JUnit":
 			return getJUnitOutput(&formattedOutput, resultsData.AdditionalJUnitData, resultsData.Verbose)
+		case "sarif":
+			return getSarifOutput(&formattedOutput, resultsData.CliVersion)
 		default:
 			panic(errors.New("invalid output format"))
 		}
@@ -184,6 +188,57 @@ func getJUnitOutput(formattedOutput *FormattedOutput, additionalJUnitData Additi
 	return convertStructToXml(FormattedOutputToJUnitOutput(*formattedOutput, additionalJUnitData, verbose))
 }
 
+func getSarifOutput(formattedOutput *FormattedOutput, cliVersion string) (string, error) {
+	// create a new report object
+	report, err := sarif.New(sarif.Version210)
+	if err != nil {
+		return "", err
+	}
+
+	const repoURL = "https://github.com/datreeio/datree"
+
+	// create a run for datree
+	run := sarif.NewRunWithInformationURI("datree", repoURL)
+	run.Tool.Driver.WithSemanticVersion(cliVersion)
+
+	for _, validationResult := range formattedOutput.PolicyValidationResults {
+		for _, ruleResult := range validationResult.RuleResults {
+			for _, occurrenceDetails := range ruleResult.OccurrencesDetails {
+				for _, failureLocation := range occurrenceDetails.FailureLocations {
+
+					helpURL := "https://hub.datree.io/built-in-rules"
+					if ruleResult.DocumentationUrl != "" {
+						helpURL = ruleResult.DocumentationUrl
+					}
+
+					howToFix := ("For information on how to fix this issue, see: " + "[" + helpURL + "](" + helpURL + ")")
+
+					// add each rule to report
+					run.AddRule(ruleResult.Identifier).
+						WithDescription(ruleResult.Name).
+						WithHelp(sarif.NewMultiformatMessageString(howToFix)).
+						WithMarkdownHelp(howToFix)
+
+					// create a result for each violation
+					result := run.CreateResultForRule(ruleResult.Identifier).WithMessage(sarif.NewTextMessage(ruleResult.MessageOnFailure))
+
+					result.AddLocation(sarif.NewLocationWithPhysicalLocation(
+						sarif.NewPhysicalLocation().WithArtifactLocation(sarif.NewSimpleArtifactLocation(validationResult.FileName)).WithRegion(sarif.NewSimpleRegion(failureLocation.FailedErrorLine, failureLocation.FailedErrorLine))))
+				}
+			}
+		}
+	}
+
+	report.AddRun(run)
+
+	marshal, err := json.MarshalIndent(report, "", "  ")
+	if err != nil {
+		return "", err
+	}
+
+	return fmt.Sprintln(string(marshal)), nil
+}
+
 func convertStructToXml(output interface{}) (string, error) {
 	xmlOutput, err := xml.MarshalIndent(output, "", "\t")
 	xmlOutput = []byte(xml.Header + string(xmlOutput))


Test patch:
diff --git a/cmd/test/main.go b/cmd/test/main.go
index fc8e237..af8a61f 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -438,6 +438,7 @@ func test(ctx *TestCommandContext, paths []string, testCommandData *TestCommandD
 		Verbose:               testCommandData.Verbose,
 		PolicyName:            testCommandData.Policy.Name,
 		K8sValidationWarnings: validationManager.k8sValidationWarningPerValidFile,
+		CliVersion:            ctx.CliVersion,
 		IsCI:                  ctx.CiContext.IsCI,
 	})
 
diff --git a/cmd/test/main_test.go b/cmd/test/main_test.go
index 1ab7965..239d047 100644
--- a/cmd/test/main_test.go
+++ b/cmd/test/main_test.go
@@ -877,12 +877,12 @@ func test_testCommand_output_flags_validation(t *testing.T, ctx *TestCommandCont
 		assert.NoError(t, err)
 	}
 
-	values := []string{" ", "Simple", "Json", "Yaml", "Xml", "junit", "invalid", "113", "true"}
+	values := []string{" ", "Simple", "Json", "Yaml", "Xml", "junit", "Sarif", "invalid", "113", "true"}
 
 	for _, value := range values {
 		err := executeTestCommand(ctx, []string{"8/*", "--output=" + value})
 		expectedErrorStr := "invalid --output option - \"" + value + "\"\n" +
-			"Valid output values are - simple, yaml, json, xml, JUnit"
+			"Valid output values are - simple, yaml, json, xml, JUnit, sarif"
 		assert.EqualError(t, err, expectedErrorStr)
 	}
 }
diff --git a/pkg/evaluation/printer_test.go b/pkg/evaluation/printer_test.go
index 4837974..c25a6f8 100644
--- a/pkg/evaluation/printer_test.go
+++ b/pkg/evaluation/printer_test.go
@@ -56,6 +56,7 @@ type expectedOutputs struct {
 	xml   string
 	yaml  string
 	JUnit string
+	sarif string
 }
 
 // TODO: fill missing call assertions
@@ -122,6 +123,9 @@ func TestCustomOutputs(t *testing.T) {
 
 	JUnitStdout, _ := getJUnitOutput(&formattedOutput, additionalJUnitData, false)
 	assert.Equal(t, expectedOutputs.JUnit, JUnitStdout)
+
+	SarifStdout, _ := getSarifOutput(&formattedOutput, "1.0.0")
+	assert.Equal(t, expectedOutputs.sarif, SarifStdout)
 }
 
 func TestCustomOutputsWithVerbose(t *testing.T) {
@@ -395,11 +399,13 @@ func getExpectedOutputs() expectedOutputs {
 	yamlOutput, _ := os.ReadFile("./printer_test_expected_outputs/yaml_output.yaml")
 	xmlOutput, _ := os.ReadFile("./printer_test_expected_outputs/xml_output.xml")
 	jUnitOutput, _ := os.ReadFile("./printer_test_expected_outputs/JUnit_output.xml")
+	sarifOutput, _ := os.ReadFile("./printer_test_expected_outputs/sarif_output.sarif")
 	return expectedOutputs{
 		json:  string(jsonOutput),
 		yaml:  string(yamlOutput),
 		xml:   string(xmlOutput),
 		JUnit: string(jUnitOutput),
+		sarif: string(sarifOutput),
 	}
 }
 
diff --git a/pkg/evaluation/printer_test_expected_outputs/sarif_output.sarif b/pkg/evaluation/printer_test_expected_outputs/sarif_output.sarif
new file mode 100644
index 0000000..124888d
--- /dev/null
+++ b/pkg/evaluation/printer_test_expected_outputs/sarif_output.sarif
@@ -0,0 +1,139 @@
+{
+  "version": "2.1.0",
+  "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json",
+  "runs": [
+    {
+      "tool": {
+        "driver": {
+          "informationUri": "https://github.com/datreeio/datree",
+          "name": "datree",
+          "rules": [
+            {
+              "id": "CONTAINERS_MISSING_IMAGE_VALUE_VERSION",
+              "shortDescription": {
+                "text": "Ensure each container image has a pinned (tag) version"
+              },
+              "help": {
+                "text": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)",
+                "markdown": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)"
+              }
+            },
+            {
+              "id": "CONTAINERS_MISSING_MEMORY_LIMIT_KEY",
+              "shortDescription": {
+                "text": "Ensure each container has a configured memory limit"
+              },
+              "help": {
+                "text": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)",
+                "markdown": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)"
+              }
+            },
+            {
+              "id": "WORKLOAD_INVALID_LABELS_VALUE",
+              "shortDescription": {
+                "text": "Ensure workload has valid label values"
+              },
+              "help": {
+                "text": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)",
+                "markdown": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)"
+              }
+            },
+            {
+              "id": "CONTAINERS_MISSING_LIVENESSPROBE_KEY",
+              "shortDescription": {
+                "text": "Ensure each container has a configured liveness probe"
+              },
+              "help": {
+                "text": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)",
+                "markdown": "For information on how to fix this issue, see: [https://hub.datree.io/built-in-rules](https://hub.datree.io/built-in-rules)"
+              }
+            }
+          ],
+          "semanticVersion": "1.0.0"
+        }
+      },
+      "results": [
+        {
+          "ruleId": "CONTAINERS_MISSING_IMAGE_VALUE_VERSION",
+          "ruleIndex": 0,
+          "message": {
+            "text": "Incorrect value for key `image` - specify an image version to avoid unpleasant \"version surprises\" in the future"
+          },
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "File1"
+                },
+                "region": {
+                  "startLine": 10,
+                  "endLine": 10
+                }
+              }
+            }
+          ]
+        },
+        {
+          "ruleId": "CONTAINERS_MISSING_MEMORY_LIMIT_KEY",
+          "ruleIndex": 1,
+          "message": {
+            "text": "Missing property object `limits.memory` - value should be within the accepted boundaries recommended by the organization"
+          },
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "File1"
+                },
+                "region": {
+                  "startLine": 95,
+                  "endLine": 95
+                }
+              }
+            }
+          ]
+        },
+        {
+          "ruleId": "WORKLOAD_INVALID_LABELS_VALUE",
+          "ruleIndex": 2,
+          "message": {
+            "text": "Incorrect value for key(s) under `labels` - the vales syntax is not valid so the Kubernetes engine will not accept it"
+          },
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "File1"
+                },
+                "region": {
+                  "startLine": 7,
+                  "endLine": 7
+                }
+              }
+            }
+          ]
+        },
+        {
+          "ruleId": "CONTAINERS_MISSING_LIVENESSPROBE_KEY",
+          "ruleIndex": 3,
+          "message": {
+            "text": "Missing property object `livenessProbe` - add a properly configured livenessProbe to catch possible deadlocks"
+          },
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "File1"
+                },
+                "region": {
+                  "startLine": 22,
+                  "endLine": 22
+                }
+              }
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}