From 1cd45961db1e17fffec6c15b716f301efe79034f Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Tue, 9 Nov 2021 09:16:49 -0300 Subject: [PATCH] printresults: add tests to cover output and Sonarqube output type This commit add new tests to cover Sonarqube output type and add asserts to check if what was printed is correctly. The tests was changed to use table testings approach to make more easily to add a new testcase. The PrintResults implementation was improved too. Basically a new io.Writer field was added to customize where we will write outputs. The default constructor will always write to Stdout, but on tests we use a custom BufferString to write. This commit also make some improvements on code organization and private method names. Updates #718 Signed-off-by: Matheus Alcantara --- .../controllers/printresults/print_results.go | 236 +++--- .../printresults/print_results_test.go | 726 +++++++++++++++--- 2 files changed, 726 insertions(+), 236 deletions(-) diff --git a/internal/controllers/printresults/print_results.go b/internal/controllers/printresults/print_results.go index 23a75e7bc..5c2abc521 100644 --- a/internal/controllers/printresults/print_results.go +++ b/internal/controllers/printresults/print_results.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -25,7 +26,7 @@ import ( "github.com/ZupIT/horusec-devkit/pkg/entities/analysis" "github.com/ZupIT/horusec-devkit/pkg/entities/vulnerability" "github.com/ZupIT/horusec-devkit/pkg/enums/severities" - enumsVulnerability "github.com/ZupIT/horusec-devkit/pkg/enums/vulnerability" + vulnerabilityenum "github.com/ZupIT/horusec-devkit/pkg/enums/vulnerability" "github.com/ZupIT/horusec-devkit/pkg/utils/logger" "github.com/ZupIT/horusec/config" @@ -49,19 +50,26 @@ type analysisOutputJSON struct { analysis.Analysis } +// PrintResults is reponsable to print results of an analysis +// to a given io.Writer. type PrintResults struct { analysis *analysis.Analysis - configs *config.Config + config *config.Config totalVulns int sonarqubeService SonarQubeConverter textOutput string + writer io.Writer } -func NewPrintResults(entity *analysis.Analysis, configs *config.Config) *PrintResults { +// NewPrintResults create a new PrintResults using os.Stdout as writer. +func NewPrintResults(entity *analysis.Analysis, cfg *config.Config) *PrintResults { return &PrintResults{ analysis: entity, - configs: configs, + config: cfg, sonarqubeService: sonarqube.NewSonarQube(entity), + writer: os.Stdout, + totalVulns: 0, + textOutput: "", } } @@ -70,7 +78,7 @@ func (pr *PrintResults) SetAnalysis(entity *analysis.Analysis) { } func (pr *PrintResults) Print() (totalVulns int, err error) { - if err := pr.factoryPrintByType(); err != nil { + if err := pr.printByOutputType(); err != nil { return 0, err } @@ -78,34 +86,34 @@ func (pr *PrintResults) Print() (totalVulns int, err error) { pr.verifyRepositoryAuthorizationToken() pr.printResponseAnalysis() pr.checkIfExistsErrorsInAnalysis() - if pr.configs.IsTimeout { + if pr.config.IsTimeout { logger.LogWarnWithLevel(messages.MsgWarnTimeoutOccurs) } return pr.totalVulns, nil } -func (pr *PrintResults) factoryPrintByType() error { +func (pr *PrintResults) printByOutputType() error { switch { - case pr.configs.PrintOutputType == outputtype.JSON: - return pr.runPrintResultsJSON() - case pr.configs.PrintOutputType == outputtype.SonarQube: - return pr.runPrintResultsSonarQube() + case pr.config.PrintOutputType == outputtype.JSON: + return pr.printResultsJSON() + case pr.config.PrintOutputType == outputtype.SonarQube: + return pr.printResultsSonarQube() default: - return pr.runPrintResultsText() + return pr.printResultsText() } } -func (pr *PrintResults) runPrintResultsText() error { - fmt.Print("\n") +func (pr *PrintResults) printResultsText() error { + fmt.Fprint(pr.writer, "\n") pr.logSeparator(true) - pr.printLNF("HORUSEC ENDED THE ANALYSIS WITH STATUS OF \"%s\" AND WITH THE FOLLOWING RESULTS:", pr.analysis.Status) + pr.printlnf(`HORUSEC ENDED THE ANALYSIS WITH STATUS OF %q AND WITH THE FOLLOWING RESULTS:`, pr.analysis.Status) pr.logSeparator(true) - pr.printLNF("Analysis StartedAt: %s", pr.analysis.CreatedAt.Format("2006-01-02 15:04:05")) - pr.printLNF("Analysis FinishedAt: %s", pr.analysis.FinishedAt.Format("2006-01-02 15:04:05")) + pr.printlnf("Analysis StartedAt: %s", pr.analysis.CreatedAt.Format("2006-01-02 15:04:05")) + pr.printlnf("Analysis FinishedAt: %s", pr.analysis.FinishedAt.Format("2006-01-02 15:04:05")) pr.logSeparator(true) @@ -114,22 +122,33 @@ func (pr *PrintResults) runPrintResultsText() error { return pr.createTxtOutputFile() } -func (pr *PrintResults) runPrintResultsJSON() error { +func (pr *PrintResults) printResultsJSON() error { a := analysisOutputJSON{ Analysis: *pr.analysis, - Version: pr.configs.Version, + Version: pr.config.Version, } - bytesToWrite, err := json.MarshalIndent(a, "", " ") + b, err := json.MarshalIndent(a, "", " ") if err != nil { logger.LogErrorWithLevel(messages.MsgErrorGenerateJSONFile, err) return err } - return pr.parseFilePathToAbsAndCreateOutputJSON(bytesToWrite) + + return pr.createOutputJSON(b) } -func (pr *PrintResults) runPrintResultsSonarQube() error { - return pr.saveSonarQubeFormatResults() +func (pr *PrintResults) printResultsSonarQube() error { + logger.LogInfoWithLevel(messages.MsgInfoStartGenerateSonarQubeFile) + + report := pr.sonarqubeService.ConvertVulnerabilityToSonarQube() + + b, err := json.MarshalIndent(report, "", " ") + if err != nil { + logger.LogErrorWithLevel(messages.MsgErrorGenerateJSONFile, err) + return err + } + + return pr.createOutputJSON(b) } func (pr *PrintResults) checkIfExistVulnerabilityOrNoSec() { @@ -150,34 +169,20 @@ func (pr *PrintResults) validateVulnerabilityToCheckTotalErrors(vuln *vulnerabil } func (pr *PrintResults) isTypeVulnToSkip(vuln *vulnerability.Vulnerability) bool { - return vuln.Type == enumsVulnerability.FalsePositive || - vuln.Type == enumsVulnerability.RiskAccepted || - vuln.Type == enumsVulnerability.Corrected + return vuln.Type == vulnerabilityenum.FalsePositive || + vuln.Type == vulnerabilityenum.RiskAccepted || + vuln.Type == vulnerabilityenum.Corrected } -func (pr *PrintResults) isIgnoredVulnerability(vulnerabilityType string) (ignore bool) { - ignore = false - - for _, typeToIgnore := range pr.configs.SeveritiesToIgnore { +func (pr *PrintResults) isIgnoredVulnerability(vulnerabilityType string) bool { + for _, typeToIgnore := range pr.config.SeveritiesToIgnore { if strings.EqualFold(vulnerabilityType, strings.TrimSpace(typeToIgnore)) || vulnerabilityType == string(severities.Info) { - ignore = true - return ignore + return true } } - return ignore -} - -func (pr *PrintResults) saveSonarQubeFormatResults() error { - logger.LogInfoWithLevel(messages.MsgInfoStartGenerateSonarQubeFile) - report := pr.sonarqubeService.ConvertVulnerabilityToSonarQube() - bytesToWrite, err := json.MarshalIndent(report, "", " ") - if err != nil { - logger.LogErrorWithLevel(messages.MsgErrorGenerateJSONFile, err) - return err - } - return pr.parseFilePathToAbsAndCreateOutputJSON(bytesToWrite) + return false } func (pr *PrintResults) returnDefaultErrOutputJSON(err error) error { @@ -185,32 +190,38 @@ func (pr *PrintResults) returnDefaultErrOutputJSON(err error) error { return ErrOutputJSON } -func (pr *PrintResults) parseFilePathToAbsAndCreateOutputJSON(bytesToWrite []byte) error { - completePath, err := filepath.Abs(pr.configs.JSONOutputFilePath) +//nolint:funlen +func (pr *PrintResults) createOutputJSON(content []byte) error { + path, err := filepath.Abs(pr.config.JSONOutputFilePath) if err != nil { return pr.returnDefaultErrOutputJSON(err) } - if _, err := os.Create(completePath); err != nil { - return pr.returnDefaultErrOutputJSON(err) - } - logger.LogInfoWithLevel(messages.MsgInfoStartWriteFile + completePath) - return pr.openJSONFileAndWriteBytes(bytesToWrite, completePath) -} -//nolint:gomnd // magic number -func (pr *PrintResults) openJSONFileAndWriteBytes(bytesToWrite []byte, completePath string) error { - outputFile, err := os.OpenFile(completePath, os.O_CREATE|os.O_WRONLY, 0600) + f, err := os.Create(path) if err != nil { return pr.returnDefaultErrOutputJSON(err) } - if err = outputFile.Truncate(0); err != nil { + + logger.LogInfoWithLevel(messages.MsgInfoStartWriteFile + path) + + if err := pr.truncateAndWriteFile(content, f); err != nil { + return err + } + + return f.Close() +} + +func (pr *PrintResults) truncateAndWriteFile(content []byte, f *os.File) error { + if err := f.Truncate(0); err != nil { return pr.returnDefaultErrOutputJSON(err) } - bytesWritten, err := outputFile.Write(bytesToWrite) - if err != nil || bytesWritten != len(bytesToWrite) { + + bytesWritten, err := f.Write(content) + if err != nil || bytesWritten != len(content) { return pr.returnDefaultErrOutputJSON(err) } - return outputFile.Close() + + return nil } func (pr *PrintResults) printTextOutputVulnerability() { @@ -222,37 +233,44 @@ func (pr *PrintResults) printTextOutputVulnerability() { pr.printTotalVulnerabilities() } +//nolint:funlen func (pr *PrintResults) printTotalVulnerabilities() { totalVulnerabilities := pr.analysis.GetTotalVulnerabilities() if totalVulnerabilities > 0 { - pr.printLNF("In this analysis, a total of %v possible vulnerabilities "+ - "were found and we classified them into:", totalVulnerabilities) + pr.printlnf( + "In this analysis, a total of %v possible vulnerabilities were found and we classified them into:", + totalVulnerabilities, + ) } - totalVulnerabilitiesBySeverity := pr.GetTotalVulnsBySeverity() + + totalVulnerabilitiesBySeverity := pr.getTotalVulnsBySeverity() for vulnType, countBySeverity := range totalVulnerabilitiesBySeverity { for severityName, count := range countBySeverity { if count > 0 { - pr.printLNF("Total of %s %s is: %v", vulnType.ToString(), severityName.ToString(), count) + pr.printlnf("Total of %s %s is: %v", vulnType.ToString(), severityName.ToString(), count) } } } } -func (pr *PrintResults) GetTotalVulnsBySeverity() (total map[enumsVulnerability.Type]map[severities.Severity]int) { - total = pr.getDefaultTotalVulnBySeverity() +func (pr *PrintResults) getTotalVulnsBySeverity() map[vulnerabilityenum.Type]map[severities.Severity]int { + total := pr.getDefaultTotalVulnBySeverity() + for index := range pr.analysis.AnalysisVulnerabilities { vuln := pr.analysis.AnalysisVulnerabilities[index].Vulnerability total[vuln.Type][vuln.Severity]++ } + return total } -func (pr *PrintResults) getDefaultTotalVulnBySeverity() map[enumsVulnerability.Type]map[severities.Severity]int { - return map[enumsVulnerability.Type]map[severities.Severity]int{ - enumsVulnerability.Vulnerability: pr.getDefaultCountBySeverity(), - enumsVulnerability.RiskAccepted: pr.getDefaultCountBySeverity(), - enumsVulnerability.FalsePositive: pr.getDefaultCountBySeverity(), - enumsVulnerability.Corrected: pr.getDefaultCountBySeverity(), +func (pr *PrintResults) getDefaultTotalVulnBySeverity() map[vulnerabilityenum.Type]map[severities.Severity]int { + count := pr.getDefaultCountBySeverity() + return map[vulnerabilityenum.Type]map[severities.Severity]int{ + vulnerabilityenum.Vulnerability: count, + vulnerabilityenum.RiskAccepted: count, + vulnerabilityenum.FalsePositive: count, + vulnerabilityenum.Corrected: count, } } @@ -269,62 +287,62 @@ func (pr *PrintResults) getDefaultCountBySeverity() map[severities.Severity]int // nolint func (pr *PrintResults) printTextOutputVulnerabilityData(vulnerability *vulnerability.Vulnerability) { - pr.printLNF("Language: %s", vulnerability.Language) - pr.printLNF("Severity: %s", vulnerability.Severity) - pr.printLNF("Line: %s", vulnerability.Line) - pr.printLNF("Column: %s", vulnerability.Column) - pr.printLNF("SecurityTool: %s", vulnerability.SecurityTool) - pr.printLNF("Confidence: %s", vulnerability.Confidence) - pr.printLNF("File: %s", pr.getProjectPath(vulnerability.File)) - pr.printLNF("Code: %s", vulnerability.Code) + pr.printlnf("Language: %s", vulnerability.Language) + pr.printlnf("Severity: %s", vulnerability.Severity) + pr.printlnf("Line: %s", vulnerability.Line) + pr.printlnf("Column: %s", vulnerability.Column) + pr.printlnf("SecurityTool: %s", vulnerability.SecurityTool) + pr.printlnf("Confidence: %s", vulnerability.Confidence) + pr.printlnf("File: %s", pr.getProjectPath(vulnerability.File)) + pr.printlnf("Code: %s", vulnerability.Code) if vulnerability.RuleID != "" { - pr.printLNF("RuleID: %s", vulnerability.RuleID) + pr.printlnf("RuleID: %s", vulnerability.RuleID) } - pr.printLNF("Details: %s", vulnerability.Details) - pr.printLNF("Type: %s", vulnerability.Type) + pr.printlnf("Details: %s", vulnerability.Details) + pr.printlnf("Type: %s", vulnerability.Type) pr.printCommitAuthor(vulnerability) - pr.printLNF("ReferenceHash: %s", vulnerability.VulnHash) + pr.printlnf("ReferenceHash: %s", vulnerability.VulnHash) pr.logSeparator(true) } // nolint func (pr *PrintResults) printCommitAuthor(vulnerability *vulnerability.Vulnerability) { - if !pr.configs.EnableCommitAuthor { + if !pr.config.EnableCommitAuthor { return } - pr.printLNF("Commit Author: %s", vulnerability.CommitAuthor) - pr.printLNF("Commit Date: %s", vulnerability.CommitDate) - pr.printLNF("Commit Email: %s", vulnerability.CommitEmail) - pr.printLNF("Commit CommitHash: %s", vulnerability.CommitHash) - pr.printLNF("Commit Message: %s", vulnerability.CommitMessage) + pr.printlnf("Commit Author: %s", vulnerability.CommitAuthor) + pr.printlnf("Commit Date: %s", vulnerability.CommitDate) + pr.printlnf("Commit Email: %s", vulnerability.CommitEmail) + pr.printlnf("Commit CommitHash: %s", vulnerability.CommitHash) + pr.printlnf("Commit Message: %s", vulnerability.CommitMessage) } func (pr *PrintResults) verifyRepositoryAuthorizationToken() { - if pr.configs.IsEmptyRepositoryAuthorization() { - fmt.Print("\n") + if pr.config.IsEmptyRepositoryAuthorization() { + fmt.Fprint(pr.writer, "\n") logger.LogWarnWithLevel(messages.MsgWarnAuthorizationNotFound) - fmt.Print("\n") + fmt.Fprint(pr.writer, "\n") } } func (pr *PrintResults) checkIfExistsErrorsInAnalysis() { - if !pr.configs.EnableInformationSeverity { + if !pr.config.EnableInformationSeverity { logger.LogWarnWithLevel(messages.MsgWarnInfoVulnerabilitiesDisabled) } if pr.analysis.HasErrors() { pr.logSeparator(true) logger.LogWarnWithLevel(messages.MsgWarnFoundErrorsInAnalysis) - fmt.Print("\n") + fmt.Fprint(pr.writer, "\n") for _, errorMessage := range strings.SplitAfter(pr.analysis.Errors, ";") { pr.printErrors(errorMessage) } - fmt.Print("\n") + fmt.Fprint(pr.writer, "\n") } } @@ -343,44 +361,46 @@ func (pr *PrintResults) printErrors(errorMessage string) { func (pr *PrintResults) printResponseAnalysis() { if pr.totalVulns > 0 { logger.LogWarnWithLevel(fmt.Sprintf(messages.MsgWarnAnalysisFoundVulns, pr.totalVulns)) - fmt.Print("\n") + fmt.Fprint(pr.writer, "\n") return } logger.LogWarnWithLevel(messages.MsgWarnAnalysisFinishedWithoutVulns) - fmt.Print("\n") + fmt.Fprint(pr.writer, "\n") } func (pr *PrintResults) logSeparator(isToShow bool) { if isToShow { - pr.printLNF("\n==================================================================================\n") + pr.printlnf("\n==================================================================================\n") } } func (pr *PrintResults) getProjectPath(path string) string { - if strings.Contains(path, pr.configs.ProjectPath) { + if strings.Contains(path, pr.config.ProjectPath) { return path } - if pr.configs.ContainerBindProjectPath != "" { - return fmt.Sprintf("%s/%s", pr.configs.ContainerBindProjectPath, path) + if pr.config.ContainerBindProjectPath != "" { + return fmt.Sprintf("%s/%s", pr.config.ContainerBindProjectPath, path) } - return fmt.Sprintf("%s/%s", pr.configs.ProjectPath, path) + return fmt.Sprintf("%s/%s", pr.config.ProjectPath, path) } -func (pr *PrintResults) printLNF(text string, args ...interface{}) { - if pr.configs.PrintOutputType == outputtype.Text { - pr.textOutput += fmt.Sprintln(fmt.Sprintf(text, args...)) +func (pr *PrintResults) printlnf(text string, args ...interface{}) { + msg := fmt.Sprintf(text, args...) + + if pr.config.PrintOutputType == outputtype.Text { + pr.textOutput += fmt.Sprintln(msg) } - fmt.Println(fmt.Sprintf(text, args...)) + fmt.Fprintln(pr.writer, msg) } func (pr *PrintResults) createTxtOutputFile() error { - if pr.configs.PrintOutputType != outputtype.Text || pr.configs.JSONOutputFilePath == "" { + if pr.config.PrintOutputType != outputtype.Text || pr.config.JSONOutputFilePath == "" { return nil } - return file.CreateAndWriteFile(pr.textOutput, pr.configs.JSONOutputFilePath) + return file.CreateAndWriteFile(pr.textOutput, pr.config.JSONOutputFilePath) } diff --git a/internal/controllers/printresults/print_results_test.go b/internal/controllers/printresults/print_results_test.go index ecae2d327..20b9510aa 100644 --- a/internal/controllers/printresults/print_results_test.go +++ b/internal/controllers/printresults/print_results_test.go @@ -15,16 +15,44 @@ package printresults import ( + "bytes" + "os" + "path/filepath" + "strings" "testing" + "github.com/ZupIT/horusec-devkit/pkg/entities/analysis" entitiesAnalysis "github.com/ZupIT/horusec-devkit/pkg/entities/analysis" + "github.com/ZupIT/horusec-devkit/pkg/entities/vulnerability" + "github.com/ZupIT/horusec-devkit/pkg/enums/confidence" + "github.com/ZupIT/horusec-devkit/pkg/enums/languages" + "github.com/ZupIT/horusec-devkit/pkg/enums/severities" + "github.com/ZupIT/horusec-devkit/pkg/enums/tools" + vulnerabilityenum "github.com/ZupIT/horusec-devkit/pkg/enums/vulnerability" + "github.com/ZupIT/horusec-devkit/pkg/utils/logger" + "github.com/ZupIT/horusec/internal/enums/outputtype" + "github.com/ZupIT/horusec/internal/helpers/messages" "github.com/ZupIT/horusec/internal/utils/mock" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/ZupIT/horusec/config" ) +type validateFn func(t *testing.T, tt testcase) + +type testcase struct { + name string + cfg config.Config + analysis analysis.Analysis + vulnerabilities int + outputs []string + err bool + validateFn validateFn +} + func TestStartPrintResultsMock(t *testing.T) { t.Run("Should return correctly mock", func(t *testing.T) { m := &Mock{} @@ -36,141 +64,583 @@ func TestStartPrintResultsMock(t *testing.T) { }) } -func TestPrintResults_StartPrintResults(t *testing.T) { - t.Run("Should not return errors with type TEXT", func(t *testing.T) { - configs := &config.Config{} - - analysis := &entitiesAnalysis.Analysis{ - AnalysisVulnerabilities: []entitiesAnalysis.AnalysisVulnerabilities{}, - } - - totalVulns, err := NewPrintResults(analysis, configs).Print() - - assert.NoError(t, err) - assert.Equal(t, 0, totalVulns) - }) - - t.Run("Should not return errors with type JSON", func(t *testing.T) { - analysis := &entitiesAnalysis.Analysis{ - AnalysisVulnerabilities: []entitiesAnalysis.AnalysisVulnerabilities{}, - } - - configs := &config.Config{} - configs.JSONOutputFilePath = "/tmp/horusec.json" - - printResults := &PrintResults{ - analysis: analysis, - configs: configs, - } - - totalVulns, err := printResults.Print() - assert.NoError(t, err) - assert.Equal(t, 0, totalVulns) - }) - - t.Run("Should return not errors because exists error in analysis", func(t *testing.T) { - analysis := &entitiesAnalysis.Analysis{ - Errors: "Exists an error when read analysis", - } - - configs := &config.Config{} - configs.PrintOutputType = "JSON" - - totalVulns, err := NewPrintResults(analysis, configs).Print() - - assert.NoError(t, err) - assert.Equal(t, 0, totalVulns) - }) - - t.Run("Should return errors with type JSON", func(t *testing.T) { - analysis := mock.CreateAnalysisMock() - - analysis.Errors += "ERROR GET REPOSITORY" - - configs := &config.Config{} - configs.PrintOutputType = "json" - - printResults := &PrintResults{ - analysis: analysis, - configs: configs, - } - - _, err := printResults.Print() - - assert.Error(t, err) - }) - - t.Run("Should return 12 vulnerabilities with timeout occurs", func(t *testing.T) { - analysisMock := mock.CreateAnalysisMock() - - analysisMock.AnalysisVulnerabilities = append(analysisMock.AnalysisVulnerabilities, entitiesAnalysis.AnalysisVulnerabilities{Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[0].Vulnerability}) - configs := &config.Config{} - configs.IsTimeout = true - printResults := &PrintResults{ - analysis: analysisMock, - configs: configs, - } - - totalVulns, err := printResults.Print() - - assert.NoError(t, err) - assert.Equal(t, 12, totalVulns) - }) - - t.Run("Should return 12 vulnerabilities", func(t *testing.T) { - analysisMock := mock.CreateAnalysisMock() - - analysisMock.AnalysisVulnerabilities = append(analysisMock.AnalysisVulnerabilities, entitiesAnalysis.AnalysisVulnerabilities{Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[0].Vulnerability}) - - printResults := &PrintResults{ - analysis: analysisMock, - configs: &config.Config{}, - } - - totalVulns, err := printResults.Print() - - assert.NoError(t, err) - assert.Equal(t, 12, totalVulns) - }) +func TestPrintResultsStartPrintResults(t *testing.T) { + testcases := []testcase{ + { + name: "Should not return error using default output type text", + cfg: config.Config{}, + analysis: entitiesAnalysis.Analysis{ + AnalysisVulnerabilities: []entitiesAnalysis.AnalysisVulnerabilities{}, + }, + }, + { + name: "Should not return error using output type json", + cfg: config.Config{ + StartOptions: config.StartOptions{ + JSONOutputFilePath: filepath.Join(t.TempDir(), "json-output.json"), + PrintOutputType: outputtype.JSON, + }, + }, + analysis: entitiesAnalysis.Analysis{ + AnalysisVulnerabilities: []entitiesAnalysis.AnalysisVulnerabilities{ + { + VulnerabilityID: uuid.MustParse("57bf7b03-b504-42ed-a026-ea89c81b7f4a"), + AnalysisID: uuid.MustParse("16c70059-aa76-4b00-87d6-ad9941f8603e"), + Vulnerability: vulnerability.Vulnerability{ + + VulnerabilityID: uuid.MustParse("54a7a2a9-d68e-4139-ba53-6bff3bc84863"), + Line: "1", + Column: "0", + Confidence: confidence.High, + File: "cert.pem", + Code: "-----BEGIN CERTIFICATE-----", + Details: "Found SSH and/or x.509 Cerficates GoSec", + SecurityTool: tools.GoSec, + Language: languages.Go, + Severity: severities.Low, + Type: vulnerabilityenum.Vulnerability, + }, + }, + }, + }, + vulnerabilities: 1, + validateFn: func(t *testing.T, tt testcase) { + assert.FileExists(t, tt.cfg.JSONOutputFilePath) - t.Run("Should return 12 vulnerabilities with commit authors", func(t *testing.T) { - configs := &config.Config{} - configs.EnableCommitAuthor = true - analysisMock := mock.CreateAnalysisMock() + json := readFile(t, tt.cfg.JSONOutputFilePath) + assert.JSONEq(t, expectedJsonResult, string(json)) + }, + }, + { + name: "Should not return error using output type sonarqube", + cfg: config.Config{ + StartOptions: config.StartOptions{ + PrintOutputType: outputtype.SonarQube, + JSONOutputFilePath: filepath.Join(t.TempDir(), "sonar-output.json"), + }, + }, + analysis: *mock.CreateAnalysisMock(), + outputs: []string{messages.MsgInfoStartGenerateSonarQubeFile}, + validateFn: func(t *testing.T, tt testcase) { + assert.FileExists(t, tt.cfg.JSONOutputFilePath) - analysisMock.AnalysisVulnerabilities = append(analysisMock.AnalysisVulnerabilities, entitiesAnalysis.AnalysisVulnerabilities{Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[0].Vulnerability}) + json := readFile(t, tt.cfg.JSONOutputFilePath) + assert.JSONEq(t, expectedSonarqubeJsonResult, string(json)) + }, + vulnerabilities: 11, + }, + { + name: "Should return not errors because exists error in analysis", + cfg: config.Config{}, + analysis: entitiesAnalysis.Analysis{ + AnalysisVulnerabilities: []entitiesAnalysis.AnalysisVulnerabilities{}, + Errors: "Exists an error when read analysis", + }, + }, + { + name: "Should return error when using json output type without output file path", + cfg: config.Config{ + StartOptions: config.StartOptions{ + PrintOutputType: outputtype.JSON, + }, + }, + analysis: *mock.CreateAnalysisMock(), + err: true, + outputs: []string{messages.MsgErrorGenerateJSONFile}, + }, + { + name: "Should return 11 vulnerabilities with timeout occurs", + cfg: config.Config{ + GlobalOptions: config.GlobalOptions{ + IsTimeout: true, + }, + }, + analysis: *mock.CreateAnalysisMock(), + vulnerabilities: 11, + outputs: []string{messages.MsgWarnTimeoutOccurs}, + }, + { + name: "Should print 11 vulnerabilities", + cfg: config.Config{}, + analysis: *mock.CreateAnalysisMock(), + vulnerabilities: 11, + }, + { + name: "Should print 11 vulnerabilities with commit authors", + cfg: config.Config{ + StartOptions: config.StartOptions{ + EnableCommitAuthor: true, + }, + }, + analysis: *mock.CreateAnalysisMock(), + vulnerabilities: 11, + outputs: []string{ + "Commit Author", "Commit Date", "Commit Email", "Commit CommitHash", "Commit Message", + }, + }, + { + name: "Should not return errors when configured to ignore vulnerabilities with severity LOW and MEDIUM", + cfg: config.Config{ + StartOptions: config.StartOptions{ + SeveritiesToIgnore: []string{"MEDIUM", "LOW"}, + }, + }, + analysis: *mock.CreateAnalysisMock(), + vulnerabilities: 3, + }, + { + name: "Should save output to file when using json output file path and text format", + cfg: config.Config{ + StartOptions: config.StartOptions{ + PrintOutputType: outputtype.Text, + JSONOutputFilePath: filepath.Join(t.TempDir(), "output"), + }, + }, + analysis: *mock.CreateAnalysisMock(), + vulnerabilities: 11, + validateFn: func(t *testing.T, tt testcase) { + assert.FileExists(t, tt.cfg.JSONOutputFilePath, "output") - totalVulns, err := NewPrintResults(analysisMock, configs).Print() + output := string(readFile(t, tt.cfg.JSONOutputFilePath)) - assert.NoError(t, err) - assert.Equal(t, 12, totalVulns) - }) + for _, line := range strings.Split(expectedTextResult, "\n") { + assert.Contains(t, output, line) + } + }, + }, + } + + for _, tt := range testcases { + t.Run(tt.name, func(t *testing.T) { + pr, output := newPrintResultsTest(&tt.analysis, &tt.cfg) + totalVulns, err := pr.Print() + + if tt.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + assert.Equal(t, tt.vulnerabilities, totalVulns) + + s := output.String() + for _, output := range tt.outputs { + assert.Contains(t, s, output) + } + + if tt.validateFn != nil { + tt.validateFn(t, tt) + } + }) + } +} - t.Run("Should not return errors when configured to ignore vulnerabilities with severity LOW and MEDIUM", func(t *testing.T) { - analysisMock := mock.CreateAnalysisMock() +// newPrintResultsTest creates a new PrintResults using the bytes.Buffer +// from return as a print results writer and logger output. +func newPrintResultsTest(entity *analysis.Analysis, cfg *config.Config) (*PrintResults, *bytes.Buffer) { + output := bytes.NewBufferString("") + pr := NewPrintResults(entity, cfg) + pr.writer = output - analysisMock.AnalysisVulnerabilities = []entitiesAnalysis.AnalysisVulnerabilities{ - { - Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[0].Vulnerability, - }, - { - Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[1].Vulnerability, - }, - { - Vulnerability: mock.CreateAnalysisMock().AnalysisVulnerabilities[2].Vulnerability, - }, - } + logger.LogSetOutput(output) - configs := &config.Config{} - configs.SeveritiesToIgnore = []string{"MEDIUM", "LOW"} + return pr, output +} - printResults := &PrintResults{ - analysis: analysisMock, - configs: configs, - } +func readFile(t *testing.T, path string) []byte { + b, err := os.ReadFile(path) + require.Nil(t, err, "Expected nil error to read file %s: %v", path, err) + return b +} - totalVulns, err := printResults.Print() - assert.NoError(t, err) - assert.Equal(t, 1, totalVulns) - }) +const ( + // expectedJsonResult is the expected json result saved on file. + expectedJsonResult = ` +{ + "version": "", + "id": "00000000-0000-0000-0000-000000000000", + "repositoryID": "00000000-0000-0000-0000-000000000000", + "repositoryName": "", + "workspaceID": "00000000-0000-0000-0000-000000000000", + "workspaceName": "", + "status": "", + "errors": "", + "createdAt": "0001-01-01T00:00:00Z", + "finishedAt": "0001-01-01T00:00:00Z", + "analysisVulnerabilities": [ + { + "vulnerabilityID": "57bf7b03-b504-42ed-a026-ea89c81b7f4a", + "analysisID": "16c70059-aa76-4b00-87d6-ad9941f8603e", + "createdAt": "0001-01-01T00:00:00Z", + "vulnerabilities": { + "vulnerabilityID": "54a7a2a9-d68e-4139-ba53-6bff3bc84863", + "line": "1", + "column": "0", + "confidence": "HIGH", + "file": "cert.pem", + "code": "-----BEGIN CERTIFICATE-----", + "details": "Found SSH and/or x.509 Cerficates GoSec", + "securityTool": "GoSec", + "language": "Go", + "severity": "LOW", + "type": "Vulnerability", + "commitAuthor": "", + "commitEmail": "", + "commitHash": "", + "commitMessage": "", + "commitDate": "", + "vulnHash": "" + } + } + ] } +` + + // expectedTextResult is the expected text result saved on file. + expectedTextResult = ` +================================================================================== + +Language: Go +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: GoSec +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates GoSec +Type: Vulnerability +ReferenceHash: e85cdcb9de69717b2c63f2367ae75c3cc0162acefc6714986eab55e6a52b0bab + +================================================================================== + +Language: C# +Severity: MEDIUM +Line: 1 +Column: 0 +SecurityTool: SecurityCodeScan +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates SecurityCodeScan +Type: Vulnerability +ReferenceHash: 3889442bd5280ea3b7bd89408d478f3d7fcfb6b78bc1e2e53e726344fc47f9bf + +================================================================================== + +Language: Ruby +Severity: HIGH +Line: 1 +Column: 0 +SecurityTool: Brakeman +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates Brakeman +Type: Vulnerability +ReferenceHash: fc9fd74b92f16d962a4758fa2b05bca09e0912e49c01d2dc5faf11a798b62480 + +================================================================================== + +Language: JavaScript +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: NpmAudit +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates NpmAudit +Type: Vulnerability +ReferenceHash: a4774674dcff66efdafe4c58df5e2cc72f768330e79187f79e93838dc7875a9e + +================================================================================== + +Language: JavaScript +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: YarnAudit +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates YarnAudit +Type: Vulnerability +ReferenceHash: 2821233bffb27450b1e24453c491a36834db50417fc70eb9d7d0af057a455126 + +================================================================================== + +Language: Python +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: Bandit +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates Bandit +Type: Vulnerability +ReferenceHash: dcff5a09607c641c7b127bf53d6efc38533f7fb601f0b00862e0d7738b25aa5d + +================================================================================== + +Language: Python +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: Safety +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates Safety +Type: Vulnerability +ReferenceHash: 1322569065b57231b2c21981a74f61710060ca967aa824c1634a791241bc5b86 + +================================================================================== + +Language: Leaks +Severity: HIGH +Line: 1 +Column: 0 +SecurityTool: HorusecEngine +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates HorusecLeaks +Type: Vulnerability +ReferenceHash: 5829ce1578d6b902c2f545181f4b8e836f29da72702fa53c0bd2caad77e869dd + +================================================================================== + +Language: Leaks +Severity: HIGH +Line: 1 +Column: 0 +SecurityTool: GitLeaks +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates GitLeaks +Type: Vulnerability +ReferenceHash: 1cb3d3e481f1b28514f06631d31a10bab589509e3fac4354fbf210e980535f72 + +================================================================================== + +Language: Java +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: HorusecEngine +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates HorusecJava +Type: Vulnerability +ReferenceHash: b7684b5d431ba356f65e8dbe3c62ee24cd97412094b5ae205c99670ed54f883d + +================================================================================== + +Language: Kotlin +Severity: LOW +Line: 1 +Column: 0 +SecurityTool: HorusecEngine +Confidence: HIGH +File: cert.pem +Code: -----BEGIN CERTIFICATE----- +Details: Found SSH and/or x.509 Cerficates HorusecKotlin +Type: Vulnerability +ReferenceHash: 9824269893d4df5e66a4fe7f53a715117bb722910228152b04831b6d2ad19a5b + +================================================================================== + +In this analysis, a total of 11 possible vulnerabilities were found and we classified them into: +Total of False Positive HIGH is: 3 +Total of False Positive MEDIUM is: 1 +Total of False Positive LOW is: 7 +Total of Corrected HIGH is: 3 +Total of Corrected MEDIUM is: 1 +Total of Corrected LOW is: 7 +Total of Vulnerability HIGH is: 3 +Total of Vulnerability MEDIUM is: 1 +Total of Vulnerability LOW is: 7 +Total of Risk Accepted HIGH is: 3 +Total of Risk Accepted MEDIUM is: 1 +Total of Risk Accepted LOW is: 7 + +` + + // expectedSonarqubeJsonResult is the expected json result + // using Sonarqube format saved on file. + expectedSonarqubeJsonResult = ` +{ + "issues": [ + { + "type": "VULNERABILITY", + "ruleId": "GoSec", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates GoSec", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "SecurityCodeScan", + "engineId": "horusec", + "severity": "MAJOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates SecurityCodeScan", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "Brakeman", + "engineId": "horusec", + "severity": "CRITICAL", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates Brakeman", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "NpmAudit", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates NpmAudit", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "YarnAudit", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates YarnAudit", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "Bandit", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates Bandit", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "Safety", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates Safety", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "HorusecEngine", + "engineId": "horusec", + "severity": "CRITICAL", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates HorusecLeaks", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "GitLeaks", + "engineId": "horusec", + "severity": "CRITICAL", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates GitLeaks", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "HorusecEngine", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates HorusecJava", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + }, + { + "type": "VULNERABILITY", + "ruleId": "HorusecEngine", + "engineId": "horusec", + "severity": "MINOR", + "effortMinutes": 0, + "primaryLocation": { + "message": "Found SSH and/or x.509 Cerficates HorusecKotlin", + "filePath": "cert.pem", + "textRange": { + "startLine": 1, + "startColumn": 1 + } + } + } + ] +} +` +)