Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multiple file scan support #106

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 31 additions & 30 deletions cmd/kubesec/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"os"
"path/filepath"
"strings"

"github.com/controlplaneio/kubesec/pkg/ruler"
"github.com/controlplaneio/kubesec/pkg/server"
Expand All @@ -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 {
Expand All @@ -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
Expand Down
35 changes: 31 additions & 4 deletions pkg/ruler/ruleset.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -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
}

Expand All @@ -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"))
Expand Down
40 changes: 40 additions & 0 deletions test/2_regression.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions test/_helper.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down