diff --git a/.gitignore b/.gitignore index e45cb47a0bc..1288e64b13a 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,7 @@ kics.config # Results results.* +platforms.json gl-sast-results.json sonarqube-results.json cyclonedx-results.xml diff --git a/e2e/cli_test.go b/e2e/cli_test.go index 0ca6377fc9a..431f2c00aba 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -79,6 +79,10 @@ func Test_E2E_CLI(t *testing.T) { checkExpectedOutput(t, &tt, arg) } + if tt.Args.ExpectedAnalyzerResults != nil && arg < len(tt.Args.ExpectedResult) { + checkExpectedAnalyzerResults(t, &tt, arg) + } + if tt.Args.ExpectedPayload != nil { // Check payload file utils.FileCheck(t, tt.Args.ExpectedPayload[arg], tt.Args.ExpectedPayload[arg], "payload") @@ -127,6 +131,11 @@ func Test_E2E_CLI(t *testing.T) { }) } +func checkExpectedAnalyzerResults(t *testing.T, tt *testcases.TestCase, argIndex int) { + jsonFileName := tt.Args.ExpectedResult[argIndex].ResultsFile + ".json" + utils.JSONSchemaValidationFromFile(t, jsonFileName, "AnalyzerResults.json") +} + func checkExpectedOutput(t *testing.T, tt *testcases.TestCase, argIndex int) { jsonFileName := tt.Args.ExpectedResult[argIndex].ResultsFile + ".json" resultsFormats := tt.Args.ExpectedResult[argIndex].ResultsFormats @@ -211,10 +220,16 @@ func prepareTemplates() testcases.TestTemplates { remediateHelp = []string{} } + var analyzeHelp, errAH = utils.PrepareExpected("analyze_help", "fixtures/assets") + if errAH != nil { + analyzeHelp = []string{} + } + return testcases.TestTemplates{ Help: strings.Join(help, "\n"), ScanHelp: strings.Join(scanHelp, "\n"), RemediateHelp: strings.Join(remediateHelp, "\n"), + AnalyzeHelp: strings.Join(analyzeHelp, "\n"), } } diff --git a/e2e/fixtures/E2E_CLI_066_ANALYZE_RESULTS.json b/e2e/fixtures/E2E_CLI_066_ANALYZE_RESULTS.json new file mode 100644 index 00000000000..fdcc82b9a08 --- /dev/null +++ b/e2e/fixtures/E2E_CLI_066_ANALYZE_RESULTS.json @@ -0,0 +1 @@ +{"Types":["ansible","openapi"],"Exc":[],"ExpectedLOC":1233} \ No newline at end of file diff --git a/e2e/fixtures/assets/analyze_help b/e2e/fixtures/assets/analyze_help new file mode 100644 index 00000000000..840508a76ee --- /dev/null +++ b/e2e/fixtures/assets/analyze_help @@ -0,0 +1,18 @@ +Usage: + kics remediate [flags] + +Flags: + -h, --help help for analyze + --analyze-path strings paths or directories to scan + example: "./somepath,somefile.txt" + --analyze-results string points to the JSON results file of analyzer (default "platforms.json") + +Global Flags: + --ci display only log messages to CLI output (mutually exclusive with silent) + -f, --log-format string determines log format (pretty,json) (default "pretty") + --log-level string determines log level (TRACE,DEBUG,INFO,WARN,ERROR,FATAL) (default "INFO") + --log-path string path to generate log file (info.log) + --no-color disable CLI color output + --profiling string enables performance profiler that prints resource consumption metrics in the logs during the execution (CPU, MEM) + -s, --silent silence stdout messages (mutually exclusive with verbose and ci) + -v, --verbose write logs to stdout too (mutually exclusive with silent) diff --git a/e2e/fixtures/assets/help b/e2e/fixtures/assets/help index 73c4d250ec0..8643381c529 100644 --- a/e2e/fixtures/assets/help +++ b/e2e/fixtures/assets/help @@ -2,6 +2,7 @@ Usage: kics [command] Available Commands: + analyze Determines the detected platforms of a certain project generate-id Generates uuid for query help Help about any command list-platforms List supported platforms diff --git a/e2e/fixtures/schemas/result-analyzer.json b/e2e/fixtures/schemas/result-analyzer.json new file mode 100644 index 00000000000..35323437f72 --- /dev/null +++ b/e2e/fixtures/schemas/result-analyzer.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "required": [ + "types", + "exc", + "expectedloc" + ], + "properties": { + "types": { + "type": "array", + "items": { + "type": "string" + } + }, + "exc": { + "type": "array", + "items": { + "type": "string" + } + }, + "expectedloc": { + "type": "integer", + "minimum": 0 + } + } + } + \ No newline at end of file diff --git a/e2e/testcases/e2e-cli-066_analyze_command.go b/e2e/testcases/e2e-cli-066_analyze_command.go new file mode 100644 index 00000000000..f848d7a7c94 --- /dev/null +++ b/e2e/testcases/e2e-cli-066_analyze_command.go @@ -0,0 +1,23 @@ +// Package testcases provides end-to-end (E2E) testing functionality for the application. +package testcases + +// E2E-CLI-066 - KICS analyze +// should finish successfully and return exit code 0 +func init() { //nolint + testSample := TestCase{ + Name: "should perform a valid analyze [E2E-CLI-066]", + Args: args{ + Args: []cmdArgs{ + []string{"analyze", + "--analyze-path", "/path/e2e/fixtures/samples/swagger", + "--analyze-results", "/path/e2e/output/E2E_CLI_066_ANALYZE_RESULTS.json"}, + }, + ExpectedAnalyzerResults: &ResultsValidation{ + ResultsFile: "E2E_CLI_066_ANALYZE_RESULTS", + ResultsFormats: []string{"json"}, + }, + }, + WantStatus: []int{0}, + } + Tests = append(Tests, testSample) +} diff --git a/e2e/testcases/general.go b/e2e/testcases/general.go index 31718bfa5e8..c7257a602d0 100644 --- a/e2e/testcases/general.go +++ b/e2e/testcases/general.go @@ -20,18 +20,20 @@ type ResultsValidation struct { } type args struct { - Args []cmdArgs // args to pass to kics binary - ExpectedOut []string // path to file with expected output - ExpectedPayload []string - ExpectedResult []ResultsValidation - ExpectedLog LogValidation - UseMock []bool + Args []cmdArgs // args to pass to kics binary + ExpectedOut []string // path to file with expected output + ExpectedPayload []string + ExpectedResult []ResultsValidation + ExpectedAnalyzerResults *ResultsValidation + ExpectedLog LogValidation + UseMock []bool } type TestTemplates struct { Help string ScanHelp string RemediateHelp string + AnalyzeHelp string } type cmdArgs []string diff --git a/internal/console/analyze.go b/internal/console/analyze.go new file mode 100644 index 00000000000..30ccd9453ce --- /dev/null +++ b/internal/console/analyze.go @@ -0,0 +1,136 @@ +package console + +import ( + _ "embed" // Embed kics CLI img and analyze-flags + "encoding/json" + "os" + "path/filepath" + + "github.com/Checkmarx/kics/internal/console/flags" + sentryReport "github.com/Checkmarx/kics/internal/sentry" + "github.com/Checkmarx/kics/pkg/analyzer" + "github.com/Checkmarx/kics/pkg/engine/source" + "github.com/Checkmarx/kics/pkg/model" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" +) + +var ( + //go:embed assets/analyze-flags.json + analyzeFlagsListContent string +) + +const ( + perms = 0640 +) + +// NewAnalyzeCmd creates a new instance of the analyze Command +func NewAnalyzeCmd() *cobra.Command { + return &cobra.Command{ + Use: "analyze", + Short: "Determines the detected platforms of a certain project", + RunE: func(cmd *cobra.Command, args []string) error { + return analyze() + }, + } +} + +func initAnalyzeCmd(analyzeCmd *cobra.Command) error { + if err := flags.InitJSONFlags( + analyzeCmd, + analyzeFlagsListContent, + false, + source.ListSupportedPlatforms(), + source.ListSupportedCloudProviders()); err != nil { + return err + } + + if err := analyzeCmd.MarkFlagRequired(flags.AnalyzePath); err != nil { + sentryReport.ReportSentry(&sentryReport.Report{ + Message: "Failed to add command required flags", + Err: err, + Location: "func initAnalyzeCmd()", + }, true) + log.Err(err).Msg("Failed to add command required flags") + } + return nil +} + +func analyze() error { + // save the analyze parameters into the AnalyzeParameters struct + analyzeParams := getAnalyzeParameters() + + return executeAnalyze(analyzeParams) +} + +func getAnalyzeParameters() *analyzer.Parameters { + analyzeParams := analyzer.Parameters{ + Path: flags.GetMultiStrFlag(flags.AnalyzePath), + Results: flags.GetStrFlag(flags.AnalyzeResults), + } + + return &analyzeParams +} + +func executeAnalyze(analyzeParams *analyzer.Parameters) error { + log.Debug().Msg("console.scan()") + + for _, warn := range warnings { + log.Warn().Msgf(warn) + } + + console := newConsole() + + console.preScan() + + analyzerStruct := &analyzer.Analyzer{ + Paths: analyzeParams.Path, + Types: []string{""}, + ExcludeTypes: []string{""}, + Exc: []string{""}, + ExcludeGitIgnore: false, + GitIgnoreFileName: "", + } + + analyzedPaths, err := analyzer.Analyze(analyzerStruct) + + if err != nil { + log.Err(err) + return err + } + + err = writeToFile(analyzeParams.Results, analyzedPaths) + + if err != nil { + log.Err(err) + return err + } + + return nil +} + +func writeToFile(resultsPath string, analyzerResults model.AnalyzedPaths) error { + err := os.MkdirAll(filepath.Dir(resultsPath), perms) + if err != nil { + return err + } + + f, err := os.Create(resultsPath) + if err != nil { + return err + } + + defer f.Close() + + content, err := json.Marshal(analyzerResults) + if err != nil { + return err + } + + _, err = f.Write(content) + if err != nil { + return err + } + + return nil +} diff --git a/internal/console/assets/analyze-flags.json b/internal/console/assets/analyze-flags.json new file mode 100644 index 00000000000..da64f49da87 --- /dev/null +++ b/internal/console/assets/analyze-flags.json @@ -0,0 +1,14 @@ +{ + "analyze-path": { + "flagType": "multiStr", + "shorthandFlag": "", + "defaultValue": null, + "usage": "paths or directories to scan\nexample: \"./somepath,somefile.txt\"" + }, + "analyze-results": { + "flagType": "str", + "shorthandFlag": "", + "defaultValue": "platforms.json", + "usage": "points to the JSON results file of analyzer" + } +} diff --git a/internal/console/flags/analyze_flags.go b/internal/console/flags/analyze_flags.go new file mode 100644 index 00000000000..137572c799c --- /dev/null +++ b/internal/console/flags/analyze_flags.go @@ -0,0 +1,7 @@ +package flags + +// Flags constants for analyze +const ( + AnalyzeResults = "analyze-results" + AnalyzePath = "analyze-path" +) diff --git a/internal/console/kics.go b/internal/console/kics.go index 67ab79ba51e..57ff55a3913 100644 --- a/internal/console/kics.go +++ b/internal/console/kics.go @@ -43,11 +43,13 @@ func NewKICSCmd() *cobra.Command { func initialize(rootCmd *cobra.Command) error { scanCmd := NewScanCmd() remediateCmd := NewRemediateCmd() + analyzeCmd := NewAnalyzeCmd() rootCmd.AddCommand(NewVersionCmd()) rootCmd.AddCommand(NewGenerateIDCmd()) rootCmd.AddCommand(scanCmd) rootCmd.AddCommand(NewListPlatformsCmd()) rootCmd.AddCommand(remediateCmd) + rootCmd.AddCommand(analyzeCmd) rootCmd.CompletionOptions.DisableDefaultCmd = true if err := flags.InitJSONFlags( @@ -67,6 +69,10 @@ func initialize(rootCmd *cobra.Command) error { return err } + if err := initAnalyzeCmd(analyzeCmd); err != nil { + return err + } + return initScanCmd(scanCmd) } diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 8e53d32887c..049bbdc57b2 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -114,6 +114,11 @@ const ( knative = "knative" ) +type Parameters struct { + Results string + Path []string +} + // regexSlice is a struct to contain a slice of regex type regexSlice struct { regex []*regexp.Regexp