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 + } + } + } + ] +} +` +)