diff --git a/.github/workflows/go-ci-integration.yml b/.github/workflows/go-ci-integration.yml index fd7aca155a5..47c277d4383 100644 --- a/.github/workflows/go-ci-integration.yml +++ b/.github/workflows/go-ci-integration.yml @@ -50,7 +50,7 @@ jobs: run: | docker run -v ${PWD}/assets/queries:/path \ -e SENTRY_DSN=${{secrets.SENTRY_DSN}} \ - kics:${{ github.sha }} --silent --log-level DEBUG --log-file --log-path "/path/info.log" -p "/path" -o "/path/results.json" + kics:${{ github.sha }} --silent --ignore-on-exit "results" --log-level DEBUG --log-file --log-path "/path/info.log" -p "/path" -o "/path/results.json" - name: Archive test logs uses: actions/upload-artifact@v2 if: always() diff --git a/cmd/console/main.go b/cmd/console/main.go index 1a58aabe37b..9a2fc4c3b2a 100644 --- a/cmd/console/main.go +++ b/cmd/console/main.go @@ -4,10 +4,14 @@ import ( "os" "github.com/Checkmarx/kics/internal/console" + "github.com/Checkmarx/kics/internal/console/helpers" + "github.com/Checkmarx/kics/internal/constants" ) func main() { // nolint:funlen,gocyclo if err := console.Execute(); err != nil { - os.Exit(1) + if helpers.ShowError("errors") { + os.Exit(constants.EngineErrorCode) + } } } diff --git a/docs/results.md b/docs/results.md index 84448c19791..d2b469563b0 100644 --- a/docs/results.md +++ b/docs/results.md @@ -299,3 +299,22 @@ SARIF reports are sorted by severity (from high to info), following [SARIF v2.1. HTML reports are sorted by severity (from high to info), the results will have query information, a list of files which vulnerability was found and a code snippet where the problem was detected as you can see in following example: + +# Exit Status Code + +## Results Status Code + +| Code | Description | +| --------------| ------------------------------------------| +| `0` | No Results were Found | +| `50` | Found any `HIGH` Results | +| `40` | Found any `MEDIUM` Results | +| `30` | Found any `LOW` Results | +| `20` | Found any `INFO` Results | + +## Error Status Code + +| Code | Description | +| --------------| ------------------------------------------| +| `126` | Engine Error | +| `130` | Signal-Interrupt | diff --git a/docs/usage/commands.md b/docs/usage/commands.md index 90f43fb10c2..18a9c675cf8 100644 --- a/docs/usage/commands.md +++ b/docs/usage/commands.md @@ -50,6 +50,12 @@ Flags: -x, --exclude-results strings exclude results by providing the similarity ID of a result can be provided multiple times or as a comma separated string example: 'fec62a97d569662093dbb9739360942f...,31263s5696620s93dbb973d9360942fc2a...' + --fail-on which kind of results should return an exit code different from 0 + accetps: high, medium, low and info + example: "high,low" + --ignoreOnExitFlag defines which kind of non-zero exits code should be ignored + accepts: all, results, errors, none + example: if 'results' is set, only engine errors will make KICS exit code different from 0 -h, --help help for scan --minimal-ui simplified version of CLI output --no-progress hides the progress bar diff --git a/e2e/cli_test.go b/e2e/cli_test.go index c685a838e6c..c5b977bcb5b 100644 --- a/e2e/cli_test.go +++ b/e2e/cli_test.go @@ -70,7 +70,7 @@ var tests = []struct { }, expectedOut: []string{"E2E_CLI_003"}, }, - wantStatus: 1, + wantStatus: 126, }, // E2E-CLI-004 - KICS scan command had a mandatory flag -p the CLI should exhibit // an error message and return exit code 1 @@ -88,7 +88,7 @@ var tests = []struct { "E2E_CLI_004", }, }, - wantStatus: 1, + wantStatus: 126, }, // E2E-CLI-005 - KICS scan with -- payload-path flag should create a file with the // passed name containing the payload of the files scanned @@ -106,7 +106,7 @@ var tests = []struct { "E2E_CLI_005_PAYLOAD", }, }, - wantStatus: 0, + wantStatus: 50, removePayload: []string{"payload.json"}, }, } diff --git a/e2e/fixtures/E2E_CLI_002 b/e2e/fixtures/E2E_CLI_002 index 447cedb221d..fdb4a0524bb 100644 --- a/e2e/fixtures/E2E_CLI_002 +++ b/e2e/fixtures/E2E_CLI_002 @@ -17,7 +17,13 @@ Flags: -x, --exclude-results strings exclude results by providing the similarity ID of a result can be provided multiple times or as a comma separated string example: 'fec62a97d569662093dbb9739360942f...,31263s5696620s93dbb973d9360942fc2a...' + --fail-on strings which kind of results should return an exit code different from 0 + accetps: high, medium, low and info + example: "high,low" (default [high,medium,low,info]) -h, --help help for scan + --ignore-on-exit string defines which kind of non-zero exits code should be ignored + accepts: all, results, errors, none + example: if 'results' is set, only engine errors will make KICS exit code different from 0 (default "none") --minimal-ui simplified version of CLI output --no-progress hides the progress bar -o, --output-path string directory path to store reports diff --git a/e2e/fixtures/E2E_CLI_003 b/e2e/fixtures/E2E_CLI_003 index 49a577393a9..bd1fa4bcabf 100644 --- a/e2e/fixtures/E2E_CLI_003 +++ b/e2e/fixtures/E2E_CLI_003 @@ -16,7 +16,13 @@ Flags: -x, --exclude-results strings exclude results by providing the similarity ID of a result can be provided multiple times or as a comma separated string example: 'fec62a97d569662093dbb9739360942f...,31263s5696620s93dbb973d9360942fc2a...' + --fail-on strings which kind of results should return an exit code different from 0 + accetps: high, medium, low and info + example: "high,low" (default [high,medium,low,info]) -h, --help help for scan + --ignore-on-exit string defines which kind of non-zero exits code should be ignored + accepts: all, results, errors, none + example: if 'results' is set, only engine errors will make KICS exit code different from 0 (default "none") --minimal-ui simplified version of CLI output --no-progress hides the progress bar -o, --output-path string directory path to store reports diff --git a/e2e/fixtures/E2E_CLI_004 b/e2e/fixtures/E2E_CLI_004 index 53cfe7f5607..0c02de5e63d 100644 --- a/e2e/fixtures/E2E_CLI_004 +++ b/e2e/fixtures/E2E_CLI_004 @@ -16,7 +16,13 @@ Flags: -x, --exclude-results strings exclude results by providing the similarity ID of a result can be provided multiple times or as a comma separated string example: 'fec62a97d569662093dbb9739360942f...,31263s5696620s93dbb973d9360942fc2a...' + --fail-on strings which kind of results should return an exit code different from 0 + accetps: high, medium, low and info + example: "high,low" (default [high,medium,low,info]) -h, --help help for scan + --ignore-on-exit string defines which kind of non-zero exits code should be ignored + accepts: all, results, errors, none + example: if 'results' is set, only engine errors will make KICS exit code different from 0 (default "none") --minimal-ui simplified version of CLI output --no-progress hides the progress bar -o, --output-path string directory path to store reports diff --git a/internal/console/helpers/exit_handler.go b/internal/console/helpers/exit_handler.go new file mode 100644 index 00000000000..ac7c936bcbd --- /dev/null +++ b/internal/console/helpers/exit_handler.go @@ -0,0 +1,71 @@ +package helpers + +import ( + "fmt" + "strings" + + "github.com/Checkmarx/kics/pkg/model" +) + +var shouldIgnore string +var shouldFail map[string]struct{} + +// ResultsExitCode calculate exit code base on severity of results, returns 0 if no results was reported +func ResultsExitCode(summary *model.Summary) int { + // severityArr is needed to make sure 'for' cycle is made in an ordered fashion + severityArr := []model.Severity{"HIGH", "MEDIUM", "LOW", "INFO"} + codeMap := map[model.Severity]int{"HIGH": 50, "MEDIUM": 40, "LOW": 30, "INFO": 20} + exitMap := summary.SeveritySummary.SeverityCounters + for _, severity := range severityArr { + if _, reportSeverity := shouldFail[strings.ToLower(string(severity))]; !reportSeverity { + continue + } + if exitMap[severity] > 0 { + return codeMap[severity] + } + } + return 0 +} + +// InitShouldIgnoreArg initializes what kind of errors should be used on exit codes +func InitShouldIgnoreArg(arg string) error { + validArgs := []string{"none", "all", "results", "errors"} + for _, validArg := range validArgs { + if strings.EqualFold(validArg, arg) { + shouldIgnore = strings.ToLower(arg) + return nil + } + } + return fmt.Errorf("unknown argument for --ignore-on-exit: %s\nvalid arguments:\n %s", arg, strings.Join(validArgs, "\n ")) +} + +// InitShouldFailArg initializes which kind of vulnerability severity should changes exit code +func InitShouldFailArg(args []string) error { + possibleArgs := map[string]struct{}{ + "high": {}, + "medium": {}, + "low": {}, + "info": {}, + } + if len(args) == 0 { + shouldFail = possibleArgs + return nil + } + + argsConverted := make(map[string]struct{}) + for _, arg := range args { + if _, ok := possibleArgs[strings.ToLower(arg)]; !ok { + validArgs := []string{"high", "medium", "low", "info"} + return fmt.Errorf("unknown argument for --fail-on: %s\nvalid arguments:\n %s", arg, strings.Join(validArgs, "\n ")) + } + argsConverted[strings.ToLower(arg)] = struct{}{} + } + + shouldFail = argsConverted + return nil +} + +// ShowError returns true if should show error, otherwise returns false +func ShowError(kind string) bool { + return strings.EqualFold(shouldIgnore, "none") || (!strings.EqualFold(shouldIgnore, "all") && !strings.EqualFold(shouldIgnore, kind)) +} diff --git a/internal/console/helpers/exit_handler_test.go b/internal/console/helpers/exit_handler_test.go new file mode 100644 index 00000000000..1b2f58c9a05 --- /dev/null +++ b/internal/console/helpers/exit_handler_test.go @@ -0,0 +1,246 @@ +package helpers + +import ( + "fmt" + "testing" + + "github.com/Checkmarx/kics/pkg/model" + "github.com/Checkmarx/kics/test" + "github.com/stretchr/testify/require" +) + +type resultExitCode struct { + summary model.Summary + failOn map[string]struct{} +} + +var resultsExitCodeTests = []struct { + caseTest resultExitCode + expectedResult int +}{ + { + caseTest: resultExitCode{ + summary: test.SummaryMock, + failOn: map[string]struct{}{ + "high": {}, + "medium": {}, + "low": {}, + "info": {}, + }, + }, + expectedResult: 50, + }, + { + caseTest: resultExitCode{ + summary: test.SummaryMock, + failOn: map[string]struct{}{ + "medium": {}, + }, + }, + expectedResult: 0, + }, + { + caseTest: resultExitCode{ + summary: test.ComplexSummaryMock, + failOn: map[string]struct{}{ + "medium": {}, + }, + }, + expectedResult: 40, + }, + { + caseTest: resultExitCode{ + summary: test.ComplexSummaryMock, + failOn: map[string]struct{}{ + "high": {}, + "medium": {}, + "low": {}, + "info": {}, + }, + }, + expectedResult: 50, + }, +} + +func TestExitHandler_ResultsExitCode(t *testing.T) { + for idx, testCase := range resultsExitCodeTests { + t.Run(fmt.Sprintf("Print test case %d", idx), func(t *testing.T) { + shouldFail = testCase.caseTest.failOn + result := ResultsExitCode(&testCase.caseTest.summary) + require.Equal(t, testCase.expectedResult, result) + }) + } +} + +type initIgnoreResult struct { + wantErr bool + want string +} + +var initShouldIgnoreExitCodeTests = []struct { + caseTest string + expectedResult initIgnoreResult +}{ + { + caseTest: "NONE", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "none", + }, + }, + { + caseTest: "None", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "none", + }, + }, + { + caseTest: "none", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "none", + }, + }, + { + caseTest: "all", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "all", + }, + }, + { + caseTest: "results", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "results", + }, + }, + { + caseTest: "errors", + expectedResult: initIgnoreResult{ + wantErr: false, + want: "errors", + }, + }, + { + caseTest: "invalid", + expectedResult: initIgnoreResult{ + wantErr: true, + want: "none", + }, + }, +} + +func TestExitHandler_InitShouldIgnoreArg(t *testing.T) { + for idx, testCase := range initShouldIgnoreExitCodeTests { + t.Run(fmt.Sprintf("Print test case %d", idx), func(t *testing.T) { + shouldIgnore = "none" + err := InitShouldIgnoreArg(testCase.caseTest) + if testCase.expectedResult.wantErr { + require.NotNil(t, err) + } else { + require.Nil(t, err) + } + require.Equal(t, testCase.expectedResult.want, shouldIgnore) + }) + } +} + +type initFail struct { + wantErr bool + want map[string]struct{} +} + +var initShouldFailTests = []struct { + caseTest []string + expectedResult initFail +}{ + { + caseTest: []string{}, + expectedResult: initFail{ + wantErr: false, + want: map[string]struct{}{ + "high": {}, + "medium": {}, + "low": {}, + "info": {}, + }, + }, + }, + { + caseTest: []string{"HIGH"}, + expectedResult: initFail{ + wantErr: false, + want: map[string]struct{}{ + "high": {}, + }, + }, + }, + { + caseTest: []string{"HIGH", "Medium", "loW", "info"}, + expectedResult: initFail{ + wantErr: false, + want: map[string]struct{}{ + "high": {}, + "medium": {}, + "low": {}, + "info": {}, + }, + }, + }, + { + caseTest: []string{"invalid"}, + expectedResult: initFail{ + wantErr: true, + want: map[string]struct{}{}, + }, + }, +} + +func TestExitHandler_InitShouldFailArg(t *testing.T) { + for idx, testCase := range initShouldFailTests { + t.Run(fmt.Sprintf("Print test case %d", idx), func(t *testing.T) { + shouldFail = make(map[string]struct{}) + err := InitShouldFailArg(testCase.caseTest) + if testCase.expectedResult.wantErr { + require.NotNil(t, err) + } else { + require.Nil(t, err) + } + require.Equal(t, testCase.expectedResult.want, shouldFail) + }) + } +} + +var showResultsTests = []struct { + caseTest string + expectedResult bool +}{ + { + caseTest: "none", + expectedResult: true, + }, + { + caseTest: "all", + expectedResult: false, + }, + { + caseTest: "results", + expectedResult: true, + }, + { + caseTest: "errors", + expectedResult: false, + }, +} + +func TestExitHandler_ShowError(t *testing.T) { + for idx, testCase := range showResultsTests { + t.Run(fmt.Sprintf("Print test case %d", idx), func(t *testing.T) { + shouldIgnore = testCase.caseTest + result := ShowError("errors") + require.Equal(t, testCase.expectedResult, result) + }) + } +} diff --git a/internal/console/kics.go b/internal/console/kics.go index f06974407e6..79458f4a104 100644 --- a/internal/console/kics.go +++ b/internal/console/kics.go @@ -23,14 +23,14 @@ const ( var ( ctx = context.Background() - verbose bool + ci bool logFile bool - logPath string - logLevel string logFormat string + logLevel string + logPath string noColor bool silent bool - ci bool + verbose bool warning []string ) diff --git a/internal/console/scan.go b/internal/console/scan.go index 3d1475520d0..020a7887854 100644 --- a/internal/console/scan.go +++ b/internal/console/scan.go @@ -4,8 +4,10 @@ import ( _ "embed" // Embed kics CLI img "fmt" "os" + "os/signal" "path/filepath" "strings" + "syscall" "time" consoleHelpers "github.com/Checkmarx/kics/internal/console/helpers" @@ -34,48 +36,52 @@ import ( ) var ( - path string - queryPath string - outputPath string - payloadPath string + //go:embed img/kics-console + banner string + + cfgFile string excludeCategories []string - excludePath []string excludeIDs []string + excludePath []string excludeResults []string + failOn []string + ignoreOnExit string + min bool + noProgress bool + outputPath string + path string + payloadPath string + previewLines int + queryPath string reportFormats []string - cfgFile string - - noProgress bool - types []string - min bool - previewLines int - //go:embed img/kics-console - banner string + types []string ) const ( - scanCommandStr = "scan" - pathFlag = "path" - pathFlagShorthand = "p" configFlag = "config" - queriesPathShorthand = "q" - outputPathFlag = "output-path" - outputPathShorthand = "o" - reportFormatsFlag = "report-formats" - previewLinesFlag = "preview-lines" + excludeCategoriesFlag = "exclude-categories" excludePathsFlag = "exclude-paths" excludePathsShorthand = "e" + excludeQueriesFlag = "exclude-queries" + excludeResultsFlag = "exclude-results" + excludeResutlsShorthand = "x" + failOnFlag = "fail-on" + ignoreOnExitFlag = "ignore-on-exit" minimalUIFlag = "minimal-ui" + noProgressFlag = "no-progress" + outputPathFlag = "output-path" + outputPathShorthand = "o" + pathFlag = "path" + pathFlagShorthand = "p" payloadPathFlag = "payload-path" payloadPathShorthand = "d" + previewLinesFlag = "preview-lines" + queriesPathCmdName = "queries-path" + queriesPathShorthand = "q" + reportFormatsFlag = "report-formats" + scanCommandStr = "scan" typeFlag = "type" typeShorthand = "t" - noProgressFlag = "no-progress" - excludeQueriesFlag = "exclude-queries" - excludeResultsFlag = "exclude-results" - excludeResutlsShorthand = "x" - excludeCategoriesFlag = "exclude-categories" - queriesPathCmdName = "queries-path" ) // NewScanCmd creates a new instance of the scan Command @@ -96,6 +102,13 @@ func NewScanCmd() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { changedDefaultQueryPath := cmd.Flags().Lookup(queriesPathCmdName).Changed + if err := consoleHelpers.InitShouldIgnoreArg(ignoreOnExit); err != nil { + return err + } + if err := consoleHelpers.InitShouldFailArg(failOn); err != nil { + return err + } + gracefulShutdown() return scan(changedDefaultQueryPath) }, } @@ -195,7 +208,7 @@ func setBoundFlags(flagName string, val interface{}, cmd *cobra.Command) { } } -func initScanCmd(scanCmd *cobra.Command) { +func initScanFlags(scanCmd *cobra.Command) { scanCmd.Flags().StringVarP(&path, pathFlag, pathFlagShorthand, @@ -281,6 +294,25 @@ func initScanCmd(scanCmd *cobra.Command) { "can be provided multiple times or as a comma separated string\n"+ "example: 'Access control,Best practices'", ) + scanCmd.Flags().StringSliceVarP(&failOn, + failOnFlag, + "", + []string{"high", "medium", "low", "info"}, + "which kind of results should return an exit code different from 0\n"+ + "accetps: high, medium, low and info\n"+ + "example: \"high,low\"", + ) + scanCmd.Flags().StringVarP(&ignoreOnExit, + ignoreOnExitFlag, + "", + "none", + "defines which kind of non-zero exits code should be ignored\n"+"accepts: all, results, errors, none\n"+ + "example: if 'results' is set, only engine errors will make KICS exit code different from 0", + ) +} + +func initScanCmd(scanCmd *cobra.Command) { + initScanFlags(scanCmd) if err := scanCmd.MarkFlagRequired("path"); err != nil { sentry.CaptureException(err) @@ -447,10 +479,10 @@ func scan(changedDefaultQueryPath bool) error { fmt.Printf(elapsedStrFormat, elapsed) log.Info().Msgf(elapsedStrFormat, elapsed) - if summary.FailedToExecuteQueries > 0 { - os.Exit(1) + exitCode := consoleHelpers.ResultsExitCode(&summary) + if consoleHelpers.ShowError("results") && exitCode != 0 { + os.Exit(exitCode) } - return nil } @@ -509,3 +541,15 @@ func printOutput(outputPath, filename string, body interface{}, formats []string } return err } + +// gracefulShutdown catches signal interrupt and returns the appropriate exit code +func gracefulShutdown() { + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + if consoleHelpers.ShowError("errors") { + os.Exit(constants.SignalInterruptCode) + } + }() +} diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 85904193357..55c21f74fcb 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -23,5 +23,11 @@ const MinimumPreviewLines = 1 // MaximumPreviewLines - default maximum preview lines number const MaximumPreviewLines = 30 +// EngineErrorCode - Exit Status code for error in engine +const EngineErrorCode = 126 + +// SignalInterruptCode - Exit Status code for a signal interrupt +const SignalInterruptCode = 130 + // MaxInteger - max possible integer in golang const MaxInteger = math.MaxInt64 diff --git a/mkdocs.yml b/mkdocs.yml index 7f869bf43fb..c7395304ab9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,3 +69,10 @@ plugins: google_analytics: - UA-186818347-2 - auto + + +markdown_extensions: + - toc: + permalink: true + toc_depth: 2 + separator: "_" diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 7c9a0ec5278..acc3a99dc07 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -100,7 +100,7 @@ func (c *Parser) SupportedExtensions() model.Extensions { func validateArguments(types, validArgs []string) error { validArgs = removeDuplicateValues(validArgs) if invalidType, ok, _ := contains(types, validArgs); !ok { - return fmt.Errorf("unknown argument: %s\nvalid arguments:\n %s", invalidType, strings.Join(validArgs, "\n ")) + return fmt.Errorf("unknown argument for --type: %s\nvalid arguments:\n %s", invalidType, strings.Join(validArgs, "\n ")) } return nil }