diff --git a/cmd/kubesec/scan.go b/cmd/kubesec/scan.go index c26666584..6516d4b26 100644 --- a/cmd/kubesec/scan.go +++ b/cmd/kubesec/scan.go @@ -7,6 +7,7 @@ import ( "log" "os" "path/filepath" + "strings" "github.com/controlplaneio/kubesec/pkg/ruler" "github.com/controlplaneio/kubesec/pkg/server" @@ -28,45 +29,42 @@ func init() { rootCmd.AddCommand(scanCmd) } -// File holds the name and contents -type File struct { - fileName string - fileBytes []byte -} - -func getInput(args []string) (File, error) { - var file File +func getInput(args []string) ([]ruler.File, error) { + var files []ruler.File if len(args) == 1 && (args[0] == "-" || args[0] == "/dev/stdin") { fileBytes, err := ioutil.ReadAll(os.Stdin) if err != nil { - return file, err + return files, err } - file = File{ - fileName: "STDIN", - fileBytes: fileBytes, + file := ruler.File{ + FileName: "STDIN", + FileBytes: fileBytes, } - return file, nil + return append(files, file), nil } - filename, err := filepath.Abs(args[0]) - if err != nil { - return file, err + for _, arg := range args { + filename, err := filepath.Abs(arg) + if err != nil { + return files, err + } + fileBytes, err := ioutil.ReadFile(filename) + if err != nil { + return files, err + } + file := ruler.File{ + FileName: filename, + FileBytes: fileBytes, + } + files = append(files, file) } - fileBytes, err := ioutil.ReadFile(filename) - if err != nil { - return file, err - } - file = File{ - fileName: filename, - fileBytes: fileBytes, - } - return file, nil + return files, nil } var scanCmd = &cobra.Command{ - Use: `scan [file]`, + Use: `scan [files]`, Short: "Scans Kubernetes resource YAML or JSON", Example: ` scan ./deployment.yaml`, RunE: func(cmd *cobra.Command, args []string) error { @@ -85,18 +83,21 @@ var scanCmd = &cobra.Command{ rootCmd.SilenceErrors = true rootCmd.SilenceUsage = true - file, err := getInput(args) + files, err := getInput(args) if err != nil { return err } - - reports, err := ruler.NewRuleset(logger).Run(file.fileBytes) + reports, err := ruler.NewRuleset(logger).Run(files) if err != nil { return err } if len(reports) == 0 { - return fmt.Errorf("invalid input %s", file.fileName) + var fileNames []string + for _, f := range files { + fileNames = append(fileNames, f.FileName) + } + return fmt.Errorf("invalid inputs: %s", strings.Join(fileNames, ", ")) } var lowScore bool diff --git a/pkg/ruler/ruleset.go b/pkg/ruler/ruleset.go index e4ae3afd3..4032b91e2 100644 --- a/pkg/ruler/ruleset.go +++ b/pkg/ruler/ruleset.go @@ -27,6 +27,12 @@ type Ruleset struct { type InvalidInputError struct { } +// File holds the name and contents +type File struct { + FileName string + FileBytes []byte +} + func (e *InvalidInputError) Error() string { return fmt.Sprintf("Invalid input") } @@ -250,15 +256,36 @@ func NewRuleset(logger *zap.SugaredLogger) *Ruleset { } } -func (rs *Ruleset) Run(fileBytes []byte) ([]Report, error) { +// Run processes files +func (rs *Ruleset) Run(files []File) ([]Report, error) { + var r []Report + for _, file := range files { + rs.logger.Debugf("processing file: %s", file.FileName) + reports, err := rs.processFile(file.FileBytes) + if err != nil { + return nil, err + } + r = append(r, reports...) + } + + return r, nil +} + +func (rs *Ruleset) processFile(fileBytes []byte) ([]Report, error) { reports := make([]Report, 0) isJSON := json.Valid(fileBytes) if isJSON { + var obj map[string]interface{} + json.Unmarshal(fileBytes, &obj) + if len(obj) == 0 { + return nil, &InvalidInputError{} + } report := rs.generateReport(fileBytes) reports = append(reports, report) } else { - bits := bytes.Split(fileBytes, []byte(detectLineBreak(fileBytes)+"---"+detectLineBreak(fileBytes))) + lineBreak := detectLineBreak(fileBytes) + bits := bytes.Split(fileBytes, []byte(lineBreak+"---"+lineBreak)) for _, doc := range bits { if len(doc) < 1 { return nil, &InvalidInputError{} @@ -365,10 +392,10 @@ func (rs *Ruleset) generateReport(json []byte) Report { if len(report.Message) > 0 { return report - } else { - report.Valid = true } + report.Valid = true + // run rules in parallel ch := make(chan RuleRef, len(rs.Rules)) var wg sync.WaitGroup diff --git a/pkg/server/server.go b/pkg/server/server.go index 2d215017a..7b00d3c7c 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -106,6 +107,10 @@ func retrieveRequestData(r *http.Request) ([]byte, error) { body = body[formPrefixLen:] } + if len(body) == 0 || len(strings.TrimSpace(string(body))) == 0 { + return nil, errors.New("Invalid input: empty") + } + return body, nil } @@ -129,9 +134,15 @@ func scanHandler(logger *zap.SugaredLogger, keypath string) http.Handler { w.Write([]byte(err.Error() + "\n")) return } + files := []ruler.File{ + { + FileName: "STDIN", + FileBytes: body, + }, + } var payload interface{} - reports, err := ruler.NewRuleset(logger).Run(body) + reports, err := ruler.NewRuleset(logger).Run(files) if err != nil { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(err.Error() + "\n")) diff --git a/test/2_regression.bats b/test/2_regression.bats index 051e9574d..3950f853f 100644 --- a/test/2_regression.bats +++ b/test/2_regression.bats @@ -140,6 +140,46 @@ teardown() { assert_lt_zero_points } +@test "can read multiple inputs (yaml, local)" { + skip_if_not_local + + run _app \ + "${TEST_DIR}/asset/score-1-cap-drop-all.yml" \ + "${TEST_DIR}/asset/score-1-daemonset-default.yml" + + assert [ "$(jq -r 'length' <<<"${output}")" == "2" ] + assert [ "$(jq -r '.[0].valid' <<<"${output}")" == "true" ] + assert [ "$(jq -r '.[1].valid' <<<"${output}")" == "true" ] +} + +@test "can read multiple inputs plus invalid (yaml, local)" { + skip_if_not_local + + run _app \ + "${TEST_DIR}/asset/score-1-cap-drop-all.yml" \ + "${TEST_DIR}/asset/score-1-daemonset-default.yml" \ + "${TEST_DIR}/asset/invalid-schema.yml" + + select_obj_valid() { + jq -r ".[] | select(.object == \"${1}\") | .valid" <<<"${output}" + } + assert [ "$(jq -r 'length' <<<"${output}")" == "3" ] + assert [ "$(select_obj_valid "Deployment/demo.default")" == "false" ] + assert [ "$(select_obj_valid "DaemonSet/undefined.default")" == "true" ] + assert [ "$(select_obj_valid "Pod/security-context-demo.default")" == "true" ] +} + +@test "can read multiple inputs plus multi file (yaml, local)" { + skip_if_not_local + + run _app \ + "${TEST_DIR}/asset/score-1-cap-drop-all.yml" \ + "${TEST_DIR}/asset/score-1-daemonset-default.yml" \ + "${TEST_DIR}/asset/multi.yml" + + assert [ "$(jq -r 'length' <<<"${output}")" == "7" ] +} + @test "errors with empty file (json, local)" { skip_if_not_local diff --git a/test/_helper.bash b/test/_helper.bash index 6c3e55da4..c5775d40e 100644 --- a/test/_helper.bash +++ b/test/_helper.bash @@ -87,12 +87,11 @@ if _is_remote; then else _app() { - local ARGS="${@:-}" + local ARGS="${*}" if [[ "${BIN_DIR}" != "" ]]; then - # remove --json flags ARGS=$(echo "${ARGS}" | sed -E 's,--json,,g') fi - "${BIN_DIR}"/kubesec scan "${ARGS}"; + "${BIN_DIR}"/kubesec scan ${ARGS}; } assert_gt_zero_points() {