Skip to content

Commit

Permalink
feat: add scan command
Browse files Browse the repository at this point in the history
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
  • Loading branch information
eddycharly committed Oct 4, 2023
1 parent 52c3f10 commit eb80a21
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 171 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,10 +415,10 @@ There is no limitation in a preprocessing [jmespath](https://jmespath.site) expr

```console
# with yaml payload
./kyverno-json --payload ./testdata/foo-bar/payload.yaml --policy ./testdata/foo-bar/policy.yaml
./kyverno-json scan --payload ./testdata/foo-bar/payload.yaml --policy ./testdata/foo-bar/policy.yaml
# with json payload (and pre processing)
./kyverno-json --payload ./testdata/tf-plan/tf.plan.json --pre-process "planned_values.root_module.resources" --policy ./testdata/tf-plan/policy.yaml
./kyverno-json scan --payload ./testdata/tf-plan/tf.plan.json --pre-process "planned_values.root_module.resources" --policy ./testdata/tf-plan/policy.yaml
```

## Documentation
Expand Down
6 changes: 2 additions & 4 deletions docs/user/commands/kyverno-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ kyverno-json [flags]
### Options

```
-h, --help help for kyverno-json
--payload string Path to payload (json or yaml file)
--policy strings Path to kyverno-json policies
--pre-process strings JmesPath expression used to pre process payload
-h, --help help for kyverno-json
```

### SEE ALSO

* [kyverno-json completion](kyverno-json_completion.md) - Generate the autocompletion script for the specified shell
* [kyverno-json docs](kyverno-json_docs.md) - Generates reference documentation.
* [kyverno-json scan](kyverno-json_scan.md) - scan

25 changes: 25 additions & 0 deletions docs/user/commands/kyverno-json_scan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## kyverno-json scan

scan

### Synopsis

Apply policies to json resources

```
kyverno-json scan [flags]
```

### Options

```
-h, --help help for scan
--payload string Path to payload (json or yaml file)
--policy strings Path to kyverno-json policies
--pre-process strings JmesPath expression used to pre process payload
```

### SEE ALSO

* [kyverno-json](kyverno-json.md) - kyverno-json

2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

func main() {
root := commands.NewRootCommand()
root := commands.RootCommand()
if err := root.Execute(); err != nil {
os.Exit(1)
}
Expand Down
82 changes: 6 additions & 76 deletions pkg/commands/root.go
Original file line number Diff line number Diff line change
@@ -1,94 +1,24 @@
package commands

import (
"errors"
"fmt"

"github.com/kyverno/kyverno-json/pkg/commands/docs"
"github.com/kyverno/kyverno-json/pkg/engine/template"
jsonengine "github.com/kyverno/kyverno-json/pkg/json-engine"
"github.com/kyverno/kyverno-json/pkg/payload"
"github.com/kyverno/kyverno-json/pkg/policy"
"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/pluralize"
"github.com/kyverno/kyverno-json/pkg/commands/scan"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

type command struct {
payload string
preprocessors []string
policies []string
}

func (c *command) Run(cmd *cobra.Command, _ []string) error {
out := cmd.OutOrStdout()
fmt.Fprintln(out, "Loading policies ...")
policies, err := policy.Load(c.policies...)
if err != nil {
return err
}
fmt.Fprintln(out, "Loading payload ...")
payload, err := payload.Load(c.payload)
if err != nil {
return err
}
if payload == nil {
return errors.New("payload is `null`")
}
fmt.Fprintln(out, "Pre processing ...")
for _, preprocessor := range c.preprocessors {
result, err := template.Execute(preprocessor, payload, nil)
if err != nil {
return err
}
if result == nil {
return fmt.Errorf("prepocessor resulted in `null` payload (%s)", preprocessor)
}
payload = result
}
var resources []interface{}
if slice, ok := payload.([]interface{}); ok {
resources = slice
} else {
resources = append(resources, payload)
}
fmt.Fprintln(out, "Running", "(", "evaluating", len(resources), pluralize.Pluralize(len(resources), "resource", "resources"), "against", len(policies), pluralize.Pluralize(len(policies), "policy", "policies"), ")", "...")
e := jsonengine.New()
responses := e.Run(jsonengine.JsonEngineRequest{
Resources: resources,
Policies: policies,
})
for _, response := range responses {
resourceName, _, _ := unstructured.NestedString(response.Resource.(map[string]interface{}), "address")
if response.Failure != nil {
fmt.Fprintln(out, "-", response.Policy.Name, "/", response.Rule.Name, "/", resourceName, "ERROR:", response.Failure)
} else {
if response.Error == nil {
fmt.Fprintln(out, "-", response.Policy.Name, "/", response.Rule.Name, "/", resourceName, "PASSED")
} else {
fmt.Fprintln(out, "-", response.Policy.Name, "/", response.Rule.Name, "/", resourceName, "FAILED:", response.Error)
}
}
}
fmt.Fprintln(out, "Done")
return nil
}

func NewRootCommand() *cobra.Command {
var command command
func RootCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "kyverno-json",
Short: "kyverno-json",
Long: "kyverno-json is a CLI tool to apply policies to json resources",
Args: cobra.NoArgs,
RunE: command.Run,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
cmd.Flags().StringVar(&command.payload, "payload", "", "Path to payload (json or yaml file)")
cmd.Flags().StringSliceVar(&command.preprocessors, "pre-process", nil, "JmesPath expression used to pre process payload")
cmd.Flags().StringSliceVar(&command.policies, "policy", nil, "Path to kyverno-json policies")
cmd.AddCommand(
docs.Command(cmd),
scan.Command(cmd),
)
return cmd
}
138 changes: 50 additions & 88 deletions pkg/commands/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,97 +3,59 @@ package commands
import (
"bytes"
"io"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func Test_Execute(t *testing.T) {
tests := []struct {
name string
payload string
preprocessors []string
policies []string
wantErr bool
out string
}{{
name: "foo-bar",
payload: "../../testdata/foo-bar/payload.yaml",
policies: []string{"../../testdata/foo-bar/policy.yaml"},
out: "../../testdata/foo-bar/out.txt",
wantErr: false,
}, {
name: "jim",
payload: "../../testdata/jim/payload.json",
policies: []string{"../../testdata/jim/policy.yaml"},
out: "../../testdata/jim/out.txt",
wantErr: false,
}, {
name: "pod-no-latest",
payload: "../../testdata/pod-no-latest/payload.yaml",
policies: []string{"../../testdata/pod-no-latest/policy.yaml"},
out: "../../testdata/pod-no-latest/out.txt",
wantErr: false,
}, {
name: "pod-all-latest",
payload: "../../testdata/pod-all-latest/payload.yaml",
policies: []string{"../../testdata/pod-all-latest/policy.yaml"},
out: "../../testdata/pod-all-latest/out.txt",
wantErr: false,
}, {
name: "scripted",
payload: "../../testdata/scripted/payload.yaml",
policies: []string{"../../testdata/scripted/policy.yaml"},
out: "../../testdata/scripted/out.txt",
wantErr: false,
}, {
name: "payload-yaml",
payload: "../../testdata/payload-yaml/payload.yaml",
preprocessors: []string{"planned_values.root_module.resources"},
policies: []string{"../../testdata/payload-yaml/policy.yaml"},
out: "../../testdata/payload-yaml/out.txt",
wantErr: false,
}, {
name: "tf-plan",
payload: "../../testdata/tf-plan/tf.plan.json",
preprocessors: []string{"planned_values.root_module.resources"},
policies: []string{"../../testdata/tf-plan/policy.yaml"},
out: "../../testdata/tf-plan/out.txt",
wantErr: false,
}, {
name: "escaped",
payload: "../../testdata/escaped/payload.yaml",
policies: []string{"../../testdata/escaped/policy.yaml"},
out: "../../testdata/escaped/out.txt",
wantErr: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := NewRootCommand()
assert.NotNil(t, cmd)
var args []string
args = append(args, "--payload", tt.payload)
for _, preprocessor := range tt.preprocessors {
args = append(args, "--pre-process", preprocessor)
}
for _, policy := range tt.policies {
args = append(args, "--policy", policy)
}
args = append(args, "--payload", tt.payload)
cmd.SetArgs(args)
out := bytes.NewBufferString("")
cmd.SetOut(out)
if err := cmd.Execute(); (err != nil) != tt.wantErr {
t.Errorf("command.Run() error = %v, wantErr %v", err, tt.wantErr)
}
actual, err := io.ReadAll(out)
assert.NoError(t, err)
if tt.out != "" {
expected, err := os.ReadFile(tt.out)
assert.NoError(t, err)
assert.Equal(t, string(expected), string(actual))
}
})
}
func TestRootCommand(t *testing.T) {
cmd := RootCommand()
assert.NotNil(t, cmd)
assert.Len(t, cmd.Commands(), 2)
err := cmd.Execute()
assert.NoError(t, err)
}

func TestRootCommandWithInvalidArg(t *testing.T) {
cmd := RootCommand()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetErr(b)
cmd.SetArgs([]string{"foo"})
err := cmd.Execute()
assert.Error(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
expected := `
Error: unknown command "foo" for "kyverno-json"
Run 'kyverno-json --help' for usage.`
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
}

func TestRootCommandWithInvalidFlag(t *testing.T) {
cmd := RootCommand()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetErr(b)
cmd.SetArgs([]string{"--xxx"})
err := cmd.Execute()
assert.Error(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
expected := `Error: unknown flag: --xxx`
assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(string(out)))
}

func TestRootCommandHelp(t *testing.T) {
cmd := RootCommand()
assert.NotNil(t, cmd)
b := bytes.NewBufferString("")
cmd.SetOut(b)
cmd.SetArgs([]string{"--help"})
err := cmd.Execute()
assert.NoError(t, err)
out, err := io.ReadAll(b)
assert.NoError(t, err)
assert.True(t, strings.HasPrefix(string(out), cmd.Long))
}
21 changes: 21 additions & 0 deletions pkg/commands/scan/command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package scan

import (
"github.com/spf13/cobra"
)

func Command(root *cobra.Command) *cobra.Command {
var command options
cmd := &cobra.Command{
Use: "scan",
Short: "scan",
Long: "Apply policies to json resources",
Args: cobra.NoArgs,
SilenceUsage: true,
RunE: command.run,
}
cmd.Flags().StringVar(&command.payload, "payload", "", "Path to payload (json or yaml file)")
cmd.Flags().StringSliceVar(&command.preprocessors, "pre-process", nil, "JmesPath expression used to pre process payload")
cmd.Flags().StringSliceVar(&command.policies, "policy", nil, "Path to kyverno-json policies")
return cmd
}
Loading

0 comments on commit eb80a21

Please sign in to comment.