Repository: datreeio/datree
Pull request #545 (2022-03-30 20:05:39)

feat: add 'how to fix' link
feat #446

Hi @TzlilSwimmer123 
I've consolidated all the changes of the #521 into a single commit and also resolved the merge conflicts of the previous pull request in this. Disregard the previous pull request and review this one. Please let me know if the changes are good.

Thanks,
Akhil


Patch:
diff --git a/bl/evaluation/evaluator.go b/bl/evaluation/evaluator.go
index 922a24d..79dabae 100644
--- a/bl/evaluation/evaluator.go
+++ b/bl/evaluation/evaluator.go
@@ -217,6 +217,7 @@ func (e *Evaluator) formatEvaluationResults(evaluationResults FailedRulesByFiles
 				mapper[filePath][ruleIdentifier] = &Rule{
 					Identifier:         ruleIdentifier,
 					Name:               failedRuleData.Name,
+					DocumentationUrl:   failedRuleData.DocumentationUrl,
 					MessageOnFailure:   failedRuleData.MessageOnFailure,
 					OccurrencesDetails: []OccurrenceDetails{},
 				}
@@ -279,10 +280,10 @@ func calculateFailedRulesByFiles(currentFailedRulesByFiles FailedRulesByFiles, v
 				ruleData.Configurations = append(ruleData.Configurations, configurationData)
 				currentFailedRulesByFiles[fileName][rule.RuleIdentifier] = ruleData
 			} else {
-				currentFailedRulesByFiles[fileName][rule.RuleIdentifier] = cliClient.FailedRule{Name: rule.RuleName, MessageOnFailure: rule.MessageOnFailure, Configurations: []cliClient.Configuration{configurationData}}
+				currentFailedRulesByFiles[fileName][rule.RuleIdentifier] = cliClient.FailedRule{Name: rule.RuleName, DocumentationUrl: rule.DocumentationUrl, MessageOnFailure: rule.MessageOnFailure, Configurations: []cliClient.Configuration{configurationData}}
 			}
 		} else {
-			currentFailedRulesByFiles[fileName] = map[string]cliClient.FailedRule{rule.RuleIdentifier: {Name: rule.RuleName, MessageOnFailure: rule.MessageOnFailure, Configurations: []cliClient.Configuration{configurationData}}}
+			currentFailedRulesByFiles[fileName] = map[string]cliClient.FailedRule{rule.RuleIdentifier: {Name: rule.RuleName, DocumentationUrl: rule.DocumentationUrl, MessageOnFailure: rule.MessageOnFailure, Configurations: []cliClient.Configuration{configurationData}}}
 		}
 	}
 
diff --git a/bl/evaluation/printer.go b/bl/evaluation/printer.go
index 1d33197..bd3c704 100644
--- a/bl/evaluation/printer.go
+++ b/bl/evaluation/printer.go
@@ -31,6 +31,7 @@ type PrintResultsData struct {
 	OutputFormat          string
 	Printer               Printer
 	K8sVersion            string
+	Verbose               bool
 	PolicyName            string
 	K8sValidationWarnings validation.K8sValidationWarningPerValidFile
 }
@@ -43,6 +44,7 @@ type textOutputData struct {
 	url                   string
 	printer               Printer
 	k8sVersion            string
+	Verbose               bool
 	policyName            string
 	k8sValidationWarnings validation.K8sValidationWarningPerValidFile
 }
@@ -84,6 +86,7 @@ func PrintResults(resultsData *PrintResultsData) error {
 			printer:               resultsData.Printer,
 			k8sVersion:            resultsData.K8sVersion,
 			policyName:            resultsData.PolicyName,
+			Verbose:               resultsData.Verbose,
 			k8sValidationWarnings: resultsData.K8sValidationWarnings,
 		})
 	}
@@ -129,7 +132,7 @@ func textOutput(outputData textOutputData) error {
 		return err
 	}
 
-	warnings, err := parseToPrinterWarnings(outputData.results, outputData.invalidYamlFiles, outputData.invalidK8sFiles, pwd, outputData.k8sVersion, outputData.k8sValidationWarnings)
+	warnings, err := parseToPrinterWarnings(outputData.results, outputData.invalidYamlFiles, outputData.invalidK8sFiles, pwd, outputData.k8sVersion, outputData.k8sValidationWarnings, outputData.Verbose)
 	if err != nil {
 		fmt.Println(err)
 		return err
@@ -180,7 +183,7 @@ func parseInvalidK8sFilesToWarnings(invalidK8sFiles []*extractor.InvalidFile, k8
 	return warnings
 }
 
-func parseToPrinterWarnings(results *EvaluationResults, invalidYamlFiles []*extractor.InvalidFile, invalidK8sFiles []*extractor.InvalidFile, pwd string, k8sVersion string, k8sValidationWarnings validation.K8sValidationWarningPerValidFile) ([]printer.Warning, error) {
+func parseToPrinterWarnings(results *EvaluationResults, invalidYamlFiles []*extractor.InvalidFile, invalidK8sFiles []*extractor.InvalidFile, pwd string, k8sVersion string, k8sValidationWarnings validation.K8sValidationWarningPerValidFile, verbose bool) ([]printer.Warning, error) {
 	var warnings = []printer.Warning{}
 
 	warnings = append(warnings, parseInvalidYamlFilesToWarnings(invalidYamlFiles)...)
@@ -206,8 +209,13 @@ func parseToPrinterWarnings(results *EvaluationResults, invalidYamlFiles []*extr
 
 			for _, ruleUniqueName := range rulesUniqueNames {
 				rule := rules[ruleUniqueName]
+				var fixLink string
+				if verbose {
+					fixLink = rule.DocumentationUrl
+				}
 				failedRule := printer.FailedRule{
 					Name:               rule.Name,
+					DocumentationUrl:   fixLink,
 					Occurrences:        rule.GetOccurrencesCount(),
 					Suggestion:         rule.MessageOnFailure,
 					OccurrencesDetails: []printer.OccurrenceDetails{},
diff --git a/bl/evaluation/rule.go b/bl/evaluation/rule.go
index 16823a0..a557c0a 100644
--- a/bl/evaluation/rule.go
+++ b/bl/evaluation/rule.go
@@ -4,6 +4,7 @@ type Rule struct {
 	Identifier         string
 	Name               string
 	MessageOnFailure   string
+	DocumentationUrl   string
 	OccurrencesDetails []OccurrenceDetails
 }
 
diff --git a/bl/policy/policy_factory.go b/bl/policy/policy_factory.go
index 9e89593..1452638 100644
--- a/bl/policy/policy_factory.go
+++ b/bl/policy/policy_factory.go
@@ -17,6 +17,7 @@ type Policy struct {
 type RuleWithSchema struct {
 	RuleIdentifier   string
 	RuleName         string
+	DocumentationUrl string
 	Schema           interface{}
 	MessageOnFailure string
 }
@@ -79,15 +80,15 @@ func populateRules(policyRules []cliClient.Rule, customRules []*cliClient.Custom
 				if err != nil {
 					return nil, err
 				}
-				rules = append(rules, RuleWithSchema{rule.Identifier, customRule.Name, schema, rule.MessageOnFailure})
+				rules = append(rules, RuleWithSchema{rule.Identifier, customRule.Name, "", schema, rule.MessageOnFailure})
 			} else {
-				rules = append(rules, RuleWithSchema{rule.Identifier, customRule.Name, customRule.Schema, rule.MessageOnFailure})
+				rules = append(rules, RuleWithSchema{rule.Identifier, customRule.Name, "", customRule.Schema, rule.MessageOnFailure})
 			}
 		} else {
 			defaultRule := getDefaultRuleByIdentifier(defaultRules, rule.Identifier)
 
 			if defaultRule != nil {
-				rules = append(rules, RuleWithSchema{rule.Identifier, defaultRule.Name, defaultRule.Schema, rule.MessageOnFailure})
+				rules = append(rules, RuleWithSchema{rule.Identifier, defaultRule.Name, defaultRule.DocumentationUrl, defaultRule.Schema, rule.MessageOnFailure})
 			} else {
 				rulesIsNotCustomNorDefaultErr := fmt.Errorf("rule %s is not custom nor default", rule.Identifier)
 				return nil, rulesIsNotCustomNorDefaultErr
@@ -123,7 +124,7 @@ func createDefaultPolicy(defaultRules *internal_policy.DefaultRulesDefinitions)
 
 	for _, defaultRule := range defaultRules.Rules {
 		if defaultRule.EnabledByDefault {
-			rules = append(rules, RuleWithSchema{defaultRule.UniqueName, defaultRule.Name, defaultRule.Schema, defaultRule.MessageOnFailure})
+			rules = append(rules, RuleWithSchema{defaultRule.UniqueName, defaultRule.Name, defaultRule.DocumentationUrl, defaultRule.Schema, defaultRule.MessageOnFailure})
 		}
 	}
 
diff --git a/pkg/cliClient/evaluation.go b/pkg/cliClient/evaluation.go
index d379e72..f0c8904 100644
--- a/pkg/cliClient/evaluation.go
+++ b/pkg/cliClient/evaluation.go
@@ -147,6 +147,7 @@ type Configuration struct {
 
 type FailedRule struct {
 	Name             string          `json:"ruleName"`
+	DocumentationUrl string          `json:"DocumentationUrl"`
 	MessageOnFailure string          `json:"messageOnFailure"`
 	Configurations   []Configuration `json:"configurations"`
 }
diff --git a/pkg/printer/printer.go b/pkg/printer/printer.go
index 5328113..01f00c0 100644
--- a/pkg/printer/printer.go
+++ b/pkg/printer/printer.go
@@ -27,6 +27,7 @@ type FailedRule struct {
 	Name               string
 	Occurrences        int
 	Suggestion         string
+	DocumentationUrl   string
 	OccurrencesDetails []OccurrenceDetails
 }
 
@@ -161,6 +162,12 @@ func (p *Printer) PrintWarnings(warnings []Warning) {
 				ruleName := p.Theme.Colors.RedBold.Sprint(failedRule.Name)
 
 				fmt.Fprintf(out, "%v %v %v\n", p.Theme.Emoji.Error, ruleName, occurrences)
+
+				if failedRule.DocumentationUrl != "" {
+					howToFix := p.Theme.Colors.Cyan.Sprint(failedRule.DocumentationUrl)
+					fmt.Fprintf(out, "    How to fix: %v\n", howToFix)
+				}
+
 				for _, occurrenceDetails := range failedRule.OccurrencesDetails {
 					fmt.Fprintf(out, "    — metadata.name: %v (kind: %v)\n", p.getStringOrNotAvailable(occurrenceDetails.MetadataName), p.getStringOrNotAvailable(occurrenceDetails.Kind))
 				}


Test patch:
diff --git a/bl/evaluation/printer_test.go b/bl/evaluation/printer_test.go
index d535f8e..76f4110 100644
--- a/bl/evaluation/printer_test.go
+++ b/bl/evaluation/printer_test.go
@@ -67,7 +67,7 @@ func TestPrintResults(t *testing.T) {
 		mockedPrinter.On("PrintEvaluationSummary", mock.Anything, mock.Anything)
 
 		t.Run(tt.name, func(t *testing.T) {
-			_ = PrintResults(&PrintResultsData{tt.args.results, tt.args.invalidYamlFiles, tt.args.invalidK8sFiles, tt.args.evaluationSummary, tt.args.loginURL, tt.args.outputFormat, mockedPrinter, "1.18.0", "Default", validation.K8sValidationWarningPerValidFile{}})
+			_ = PrintResults(&PrintResultsData{tt.args.results, tt.args.invalidYamlFiles, tt.args.invalidK8sFiles, tt.args.evaluationSummary, tt.args.loginURL, tt.args.outputFormat, mockedPrinter, "1.18.0", false, "Default", validation.K8sValidationWarningPerValidFile{}})
 
 			if tt.args.outputFormat == "json" {
 				mockedPrinter.AssertNotCalled(t, "PrintWarnings")
@@ -77,7 +77,7 @@ func TestPrintResults(t *testing.T) {
 				mockedPrinter.AssertNotCalled(t, "PrintWarnings")
 			} else {
 				pwd, _ := os.Getwd()
-				warnings, _ := parseToPrinterWarnings(tt.args.results.EvaluationResults, tt.args.invalidYamlFiles, tt.args.invalidK8sFiles, pwd, "1.18.0", validation.K8sValidationWarningPerValidFile{})
+				warnings, _ := parseToPrinterWarnings(tt.args.results.EvaluationResults, tt.args.invalidYamlFiles, tt.args.invalidK8sFiles, pwd, "1.18.0", validation.K8sValidationWarningPerValidFile{}, false)
 				mockedPrinter.AssertCalled(t, "PrintWarnings", warnings)
 			}
 		})
diff --git a/bl/policy/policy_factory_test.go b/bl/policy/policy_factory_test.go
index 9d5689d..c32d3a5 100644
--- a/bl/policy/policy_factory_test.go
+++ b/bl/policy/policy_factory_test.go
@@ -37,9 +37,9 @@ func TestCreatePolicy(t *testing.T) {
 		for _, defaultRule := range defaultRules.Rules {
 			switch defaultRule.UniqueName {
 			case "WORKLOAD_INCORRECT_NAMESPACE_VALUE_DEFAULT":
-				expectedRules = append(expectedRules, RuleWithSchema{RuleIdentifier: defaultRule.UniqueName, RuleName: defaultRule.Name, Schema: defaultRule.Schema, MessageOnFailure: "Incorrect value for key `namespace` - use an explicit namespace instead of the default one (`default`)"})
+				expectedRules = append(expectedRules, RuleWithSchema{RuleIdentifier: defaultRule.UniqueName, RuleName: defaultRule.Name, DocumentationUrl: defaultRule.DocumentationUrl, Schema: defaultRule.Schema, MessageOnFailure: "Incorrect value for key `namespace` - use an explicit namespace instead of the default one (`default`)"})
 			case "CONTAINERS_INCORRECT_PRIVILEGED_VALUE_TRUE":
-				expectedRules = append(expectedRules, RuleWithSchema{RuleIdentifier: defaultRule.UniqueName, RuleName: defaultRule.Name, Schema: defaultRule.Schema, MessageOnFailure: "Incorrect value for key `privileged` - this mode will allow the container thenhjgjgj same access as processes running on the host"})
+				expectedRules = append(expectedRules, RuleWithSchema{RuleIdentifier: defaultRule.UniqueName, RuleName: defaultRule.Name, DocumentationUrl: defaultRule.DocumentationUrl, Schema: defaultRule.Schema, MessageOnFailure: "Incorrect value for key `privileged` - this mode will allow the container thenhjgjgj same access as processes running on the host"})
 			}
 		}
 
diff --git a/cmd/test/main.go b/cmd/test/main.go
index f3fb259..219cd81 100644
--- a/cmd/test/main.go
+++ b/cmd/test/main.go
@@ -47,6 +47,7 @@ type TestCommandFlags struct {
 	K8sVersion           string
 	IgnoreMissingSchemas bool
 	OnlyK8sFiles         bool
+	Verbose              bool
 	PolicyName           string
 	SchemaLocations      []string
 	PolicyConfig         string
@@ -59,6 +60,7 @@ func NewTestCommandFlags() *TestCommandFlags {
 		K8sVersion:           "",
 		IgnoreMissingSchemas: false,
 		OnlyK8sFiles:         false,
+		Verbose:              false,
 		PolicyName:           "",
 		SchemaLocations:      make([]string, 0),
 	}
@@ -113,6 +115,7 @@ type TestCommandData struct {
 	K8sVersion            string
 	IgnoreMissingSchemas  bool
 	OnlyK8sFiles          bool
+	Verbose               bool
 	Policy                policy_factory.Policy
 	SchemaLocations       []string
 	Token                 string
@@ -226,6 +229,7 @@ func (flags *TestCommandFlags) AddFlags(cmd *cobra.Command) {
 
 	cmd.Flags().StringVar(&flags.PolicyConfig, "policy-config", "", "Path for local policies configuration file")
 	cmd.Flags().BoolVar(&flags.OnlyK8sFiles, "only-k8s-files", false, "Evaluate only valid yaml files with the properties 'apiVersion' and 'kind'. Ignore everything else")
+	cmd.Flags().BoolVar(&flags.Verbose, "verbose", false, "Display 'How to Fix' link")
 
 	// kubeconform flag
 	cmd.Flags().StringArrayVarP(&flags.SchemaLocations, "schema-location", "", []string{}, "Override schemas location search path (can be specified multiple times)")
@@ -270,6 +274,7 @@ func GenerateTestCommandData(testCommandFlags *TestCommandFlags, localConfigCont
 		K8sVersion:            k8sVersion,
 		IgnoreMissingSchemas:  testCommandFlags.IgnoreMissingSchemas,
 		OnlyK8sFiles:          testCommandFlags.OnlyK8sFiles,
+		Verbose:               testCommandFlags.Verbose,
 		Policy:                policy,
 		SchemaLocations:       testCommandFlags.SchemaLocations,
 		Token:                 localConfigContent.Token,
@@ -357,6 +362,7 @@ func Test(ctx *TestCommandContext, paths []string, prerunData *TestCommandData)
 		OutputFormat:          prerunData.Output,
 		Printer:               ctx.Printer,
 		K8sVersion:            prerunData.K8sVersion,
+		Verbose:               prerunData.Verbose,
 		PolicyName:            prerunData.Policy.Name,
 		K8sValidationWarnings: validationManager.k8sValidationWarningPerValidFile,
 	})