Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Brendan Ryan committed Jul 4, 2019
0 parents commit 22830ac
Show file tree
Hide file tree
Showing 11 changed files with 662 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ccheck
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
default: build

vet:
go vet .

build:
go build .

test:
go test ./...


111 changes: 111 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# `ccheck`
---

`ccheck` is a command line application for writing tests against configuration files and data using the [`rego` query language](https://www.openpolicyagent.org/docs/latest). It was primarily written for checking Kubernetes config files, but is generic enough to be used for any structured data type (`json`, `toml` etc..)

## Usage

The `ccheck` binary uses two special `rego` declarations, `deny` and `warn` during its evaluation process. If a resource matches a `deny` rule, a failure will be issued, otherwise a `warning` will be logged to the command line. An example of a valid, well-formed `ccheck` config is as follows:

**Example `*.rego file`**

```rego
package main
is_hpa {
input.kind = "HorizontalPodAutoscaler"
}
# checks that we do not include any horizontal pod autoscalers
deny[msg] {
not is_hpa
msg = sprintf("%s must not include any Horizontal Pod AutoScalers", [input.metadata.name])
}
# checks that apps do not live in the default namespace
warn[msg] {
not input.metadata.namespace = "default"
msg = sprintf("%s should not be configured to live in the default namespace", [input.metadata.name])
```

`ccheck` can then be invoked using this policy via:

```bash
ccheck -p <policy directory> <files to check....>
```

For example using the following file:

**Example Kubernetes `.yaml` file**

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

---

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: nginx
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 50
```
Will produce the following output:
```bash
Warning: /Users/brendanjryan/projects/ccheck/example/test.yaml - nginx-deployment should not be configured to live in the default namespace
Failure: /Users/brendanjryan/projects/ccheck/example/test.yaml - nginx-deployment must not include any Horizontal Pod AutoScalers
brendanjryan@Brendans-MacBook-Pro:~/projects/ccheck|
```


**Full Example:**

If you would like to see `ccheck` in action - this project bundles this example in its source as well. Just `clone` this project and run:


```bash
./ccheck -p example/policies example/test.yaml
Warning: /Users/brendanjryan/projects/ccheck/example/test.yaml - nginx-deployment should not be configured to live in the default namespace
Failure: /Users/brendanjryan/projects/ccheck/example/test.yaml - nginx-deployment must not include any Horizontal Pod AutoScalers
brendanjryan@Brendans-MacBook-Pro:~/projects/ccheck|
```

## FAQ

- Why use `rego` instead of another declarative language like `hcl`?

Although `rego` is a very new and domain specific language, it's simple grammar and extensibility were the main motivators in using it instead of a more popular declarative language or framework. As an added bonus, you can re-use your policies declared in `rego` right out of the box in [kubernetes admission controllers](https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/) powered by [Open Policy Agent](https://www.openpolicyagent.org/)

## Additional References

- [Rego language spec](https://www.openpolicyagent.org/docs/latest)
- [Open Policy Agent Project](https://www.openpolicyagent.org/)
17 changes: 17 additions & 0 deletions example/policies/policy.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

is_hpa {
input.kind = "HorizontalPodAutoscaler"
}

# checks that we do not include any horizontal pod autoscalers
deny[msg] {
not is_hpa
msg = sprintf("%s must not include any Horizontal Pod AutoScalers", [input.metadata.name])
}

# checks that apps do not live in the default namespace
warn[msg] {
not input.metadata.namespace = "default"
msg = sprintf("%s should not be configured to live in the default namespace", [input.metadata.name])
}
37 changes: 37 additions & 0 deletions example/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

---

apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: nginx
namespace: default
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 50
22 changes: 22 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module github.com/brendanjryan/ccheck

go 1.12

require (
github.com/OneOfOne/xxhash v1.2.5 // indirect
github.com/fatih/color v1.7.0
github.com/ghodss/yaml v1.0.0
github.com/gobwas/glob v0.2.3 // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/open-policy-agent/opa v0.12.0
github.com/pkg/errors v0.8.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a // indirect
github.com/uber-go/multierr v1.1.0
github.com/urfave/cli v1.20.0
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b // indirect
go.uber.org/atomic v1.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.2
)
41 changes: 41 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
github.com/OneOfOne/xxhash v1.2.5 h1:zl/OfRA6nftbBK9qTohYBJ5xvw6C/oNKizR7cZGl3cI=
github.com/OneOfOne/xxhash v1.2.5/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/open-policy-agent/opa v0.12.0 h1:ocbBbALdMH/JXyC9w8M1P1D82gs57e0xZ4QoRRFREJQ=
github.com/open-policy-agent/opa v0.12.0/go.mod h1:rlfeSeHuZmMEpmrcGla42AjkOUjP4rGIpS96H12un3o=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/uber-go/multierr v1.1.0 h1:0yjbRFohBIYjhub5/9AKZtV0P7079XboeGwXz0ccR9w=
github.com/uber-go/multierr v1.1.0/go.mod h1:ezSsblYU20Gqx98LnaYrIdu6KxYN1ctSsX09RsSjk5Y=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY=
github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
126 changes: 126 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package main

import (
"context"
"log"
"os"

"github.com/brendanjryan/ccheck/pkg"
"github.com/fatih/color"
"github.com/urfave/cli"
)

func main() {
app := cli.NewApp()

app.Name = "ccheck"
app.Usage = "ccheck <files>"
app.Author = "Brendan Ryan"
app.Description = "A command line utility for validating structured config files"

as := args{}

app.Flags = []cli.Flag{
cli.StringFlag{
Name: "p",
Value: "policies",
Usage: "directory which policy definitions live in",
Destination: &as.policyDir,
},
cli.StringFlag{
Name: "n",
Value: "main",
Usage: "namespace of rules",
Destination: &as.namespace,
},
cli.BoolFlag{
Name: "s",
Usage: "whether or not strict mode is enabled",
Destination: &as.strict,
},
}

app.Action = func(c *cli.Context) error {
as.configs = c.Args()
err := confCheck(as)
if err != nil {
log.Fatal(cli.NewExitError("error: "+err.Error(), 1))
}

return nil
}

err := app.Run(os.Args)
if err != nil {
log.Fatal("error creating CLI application: ", err)
}

return
}

func confCheck(as args) error {
ctx := context.Background()

cc := pkg.NewConfChecker(as.namespace, as.policyDir, as.configs)

cr, err := cc.Run(ctx)
if err != nil {
log.Println("error running checker: ", err)
return err
}

p := printer{}
for f, res := range cr {
if len(res.Warnings) == 0 && len(res.Failures) == 0 {
p.ok(f)
}

for _, w := range res.Warnings {
if as.strict {
p.err(f, w)
continue
}

p.warning(f, w)
}

for _, fa := range res.Failures {
// trap an error just so we exit with the right code
err = fa
p.err(f, fa)
}
}

return nil
}

// args represents all command line arguments supported by this script
type args struct {
// the directory which policy files live in
policyDir string

// the namespace rules live in:
// https://www.openpolicyagent.org/docs/latest/how-do-i-write-policies#packages
namespace string

// whether or not strict mode is enabled
strict bool

// a list of config files we will check
configs []string
}

// printer controlls printing the results of this script in a formatted manner.
type printer struct{}

func (p printer) err(file string, err error) {
color.Red("Failure: %s - %s", file, err)
}

func (p printer) warning(file string, err error) {
color.Yellow("Warning: %s - %s", file, err)
}

func (p printer) ok(file string) {
color.Green("Passed: %s", file)
}
Loading

0 comments on commit 22830ac

Please sign in to comment.