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

Add description as additional column in Rego validation results #69

Merged
merged 4 commits into from
May 6, 2024
Merged
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
15 changes: 3 additions & 12 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// rootCommand returns a cobra command for genvalctl CLI tool
var rootCmd = &cobra.Command{
Use: "genval",
Short: "genval is a CLI tool to generate and validate configuration files",
Short: " ",
Long: `
Genval is a versatile Go utility that simplifies configuration management by generating and validating configuration files
for a wide range of tools. It supports various file types, including Dockerfile, Kubernetes manifests,
Expand All @@ -19,17 +19,8 @@ var rootCmd = &cobra.Command{
}

func init() {
color.New(color.FgHiCyan, color.Bold).Println(`

:'######:::'########:'##::: ##:'##::::'##::::'###::::'##:::::::
'##... ##:: ##.....:: ###:: ##: ##:::: ##:::'## ##::: ##:::::::
##:::..::: ##::::::: ####: ##: ##:::: ##::'##:. ##:: ##:::::::
##::'####: ######::: ## ## ##: ##:::: ##:'##:::. ##: ##:::::::
##::: ##:: ##...:::: ##. ####:. ##:: ##:: #########: ##:::::::
##::: ##:: ##::::::: ##:. ###::. ## ##::: ##.... ##: ##:::::::
. ######::: ########: ##::. ##:::. ###:::: ##:::: ##: ########:
:......::::........::..::::..:::::...:::::..:::::..::........::
`)
rootCmd.SetOut(color.Output)
rootCmd.SetErr(color.Error)
}

func Execute() {
Expand Down
14 changes: 13 additions & 1 deletion pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,5 +425,17 @@ func GetVersion() (string, error) {
if err != nil {
return "", fmt.Errorf("error running git describe: %v", err)
}
return strings.TrimSpace("Genval: " + string(version)), nil
return fmt.Sprintf(`
:'######:::'########:'##::: ##:'##::::'##::::'###::::'##:::::::
'##... ##:: ##.....:: ###:: ##: ##:::: ##:::'## ##::: ##:::::::
##:::..::: ##::::::: ####: ##: ##:::: ##::'##:. ##:: ##:::::::
##::'####: ######::: ## ## ##: ##:::: ##:'##:::. ##: ##:::::::
##::: ##:: ##...:::: ##. ####:. ##:: ##:: #########: ##:::::::
##::: ##:: ##::::::: ##:. ###::. ## ##::: ##.... ##: ##:::::::
. ######::: ########: ##::. ##:::. ###:::: ##:::: ##: ########:
:......::::........::..::::..:::::...:::::..:::::..::........::

genval is a CLI tool to generate and validate configuration files

Genval: %s`, strings.TrimSpace(string(version))), nil
}
64 changes: 64 additions & 0 deletions pkg/validate/printresults.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package validate

import (
"errors"
"fmt"
"os"

"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/open-policy-agent/opa/rego"
log "github.com/sirupsen/logrus"
)

func PrintResults(result rego.ResultSet) error {
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Policy", "Status", "Description"})
var policyError error
var desc string
for _, r := range result {
if len(r.Expressions) > 0 {
keys := r.Expressions[0].Value.(map[string]interface{})
for key, value := range keys {
switch v := value.(type) {
case []interface{}:
// Check if the slice is not empty
if len(v) > 0 {
// If it's not empty, take the first element
desc = fmt.Sprintf("%v", v[0])
}
case string:
// If value is a string, assign it to desc
desc = v
}

// Perform type assertion to check if value is a slice
if slice, ok := value.([]interface{}); ok {
// Check if the slice is empty
if len(slice) > 0 {
t.AppendRow(table.Row{key, color.New(color.FgGreen).Sprint("passed"), desc})
} else {
t.AppendRow(table.Row{key, color.New(color.FgRed).Sprint("failed"), "NA"})
policyError = errors.New("policy evaluation failed: " + key)
}
} else {
// Handle other types of values (non-slice)
if value != nil {
t.AppendRow(table.Row{key, color.New(color.FgGreen).Sprint("passed"), desc})
} else {
t.AppendRow(table.Row{key, color.New(color.FgRed).Sprint("failed"), "NA"})
policyError = errors.New("policy evaluation failed: " + key)
}
}
}
} else {
log.Error("No policies passed")
policyError = errors.New("no policies passed")
}
}

t.Render()

return policyError
}
27 changes: 3 additions & 24 deletions pkg/validate/regoval.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package validate
import (
"context"
"encoding/json"
"errors"
"os"
"fmt"

"github.com/fatih/color"
"github.com/intelops/genval/pkg/parser"
"github.com/intelops/genval/pkg/utils"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/open-policy-agent/opa/rego"
log "github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -54,26 +51,8 @@ func ValidateWithRego(inputContent string, regoPolicy string) error {
return err
}

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Policy", "Status"})

for _, result := range rs {
if len(result.Expressions) > 0 {
keys := result.Expressions[0].Value.(map[string]interface{})
for key, value := range keys {
if value != true {
t.AppendRow(table.Row{key, color.New(color.FgRed).Sprint("failed")})
} else {
t.AppendRow(table.Row{key, color.New(color.FgGreen).Sprint("passed")})
}
}
} else {
log.Error("No policies passed")
return errors.New("no policies passed")
}
if err := PrintResults(rs); err != nil {
return fmt.Errorf("error evaluating rego results fron policy %s: %v", regoPolicy, err)
}

t.Render()
return nil
}
48 changes: 27 additions & 21 deletions pkg/validate/testdata/rego/dockerfile_policies.rego
Original file line number Diff line number Diff line change
@@ -1,54 +1,60 @@
package dockerfile_validation


default untrusted_base_image = false
default deny_root_user = false
default deny_sudo = false
default deny_caching = false
default deny_add = false
# default deny_image_expansion = false
import rego.v1



untrusted_base_image {
untrusted_base_image contains msg if {
input[i].cmd == "from"
val := split(input[i].value, "/")
val[0] == "cgr.dev"
msg:= "Dockerfile uses hardened base image from Chainguard"
}

# Do not use root user
deny_root_user {
deny_root_user contains msg if {
input[i].cmd == "user"
val2:= input[i].value
val2 != "root"
val2 != "0"
msg:= "Dockerfile does not support root USER"
}

# CIS 4.1 Ensure that a user for the container has been created
user_defined contains msg if{
input[i].cmd == "user"
msg:= "Dockerfile has a USER created for the container"
}

# Do not sudo
deny_sudo{
# # Do not sudo
deny_sudo contains msg if {
input[i].cmd == "run"
val3:= input[i].value
not contains(val3, "sudo")
msg:= "Dockerfile does not support sudo"
}

# Avoid using cached layers CIS 4.7
deny_caching{
# # Avoid using cached layers CIS 4.7
deny_caching contains msg if {
input[i].cmd == "run"
val4:= input[i].value
matches := regex.match(".*?(apk|yum|dnf|apt|pip).+?(install|[dist-|check-|group]?up[grade|date]).*", val4)
matches == true
contains(val4, "--no-cache")
msg:= "Dockerfile avoids using cached layers when installing packages"
}

# Ensure that COPY is used instead of ADD CIS 4.9
deny_add{
# # Ensure that COPY is used instead of ADD CIS 4.9
deny_add contains msg if {
input[i].cmd != "add"
msg:= "Dockerfile does not use ADD instruction - CIS 4.9"
}

# Ensure ADD does not include unpack archives or download files
# deny_image_expansion{
# input[_].cmd == "add"
# val5 := input[_].value
# words := regex.match(".*?(curl|wget|.tar|.tar.).*", val5)
# words != true
# Ensure update/upgrade instructions are not used in the Dockerfile - CIS 4.7
# deny_package_update_instructions contains msg if {
# blacklist := [" update "," upgrade "]
# input[i].cmd == "run"
# val := concat(" ", input[i].value)
# not contains(val, blacklist[i])
# msg:= "Ensure update/upgrade instructions are not used in the Dockerfile - CIS 4.7"
# }
6 changes: 4 additions & 2 deletions pkg/validate/testdata/rego/fail.rego
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package dockerfile_validation

default fail = false
import rego.v1

untrusted_base_image {

untrusted_base_image contains msg if {
input[i].cmd == "from"
val := split(input[i].value, "/")
val[0] == "cgr.dev"
msg:= "Image does noyt contain latest tag"
}
7 changes: 3 additions & 4 deletions pkg/validate/testdata/rego/k8s.rego
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package validate_k8s

import future.keywords
import rego.v1

default deny_latest = false


deny_latest{
deny_latest contains msg if {
input.kind == "Deployment"
c:= input.spec.template.spec.containers[i].image
not endswith(c, "latest")
msg:= "Image does not have latest tag"
}


66 changes: 26 additions & 40 deletions pkg/validate/validatedockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ package validate
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"

"github.com/fatih/color"
"github.com/intelops/genval/pkg/parser"
"github.com/intelops/genval/pkg/utils"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
log "github.com/sirupsen/logrus"
)
Expand All @@ -19,74 +17,62 @@ import (
func ValidateDockerfile(dockerfileContent string, regoPolicyPath string) error {
dockerPolicy, err := utils.ReadFile(regoPolicyPath)
if err != nil {
log.WithError(err).Error("Error reading the policy file.")
return errors.New("error reading the policy file")
return fmt.Errorf("error reading the policy file %v: %v", regoPolicyPath, err)
}

pkg, err := utils.ExtractPackageName(dockerPolicy)
if err != nil {
log.Fatalf("Unable to fetch package name: %v", err)
return fmt.Errorf("errr fetching package name from polcy %v: %v", dockerPolicy, err)
}

// Prepare Rego input data
dockerfileInstructions := parser.ParseDockerfileContent(dockerfileContent)

jsonData, err := json.Marshal(dockerfileInstructions)
if err != nil {
log.WithError(err).Error("Error converting to JSON:", err)
return errors.New("error converting to JSON")
return fmt.Errorf("error marshalling %v, to JSON: %v", dockerfileInstructions, err)
}

policyName := filepath.Base(regoPolicyPath)

var commands []map[string]string
err = json.Unmarshal([]byte(jsonData), &commands)
if err != nil {
errWithContext := fmt.Errorf("error converting JSON to map: %v", err)
log.WithError(err).Error(errWithContext.Error())
return errWithContext
return fmt.Errorf("error converting JSON to map: %v", err)
}

ctx := context.Background()

compiler, err := ast.CompileModules(map[string]string{
policyName: string(dockerPolicy),
})
if err != nil {
log.Fatal(err)
return fmt.Errorf("failed to compile rego policy: %w", err)
}
// Create regoQuery for evaluation
regoQuery := rego.New(
rego.Query("data."+pkg),
rego.Module(regoPolicyPath, string(dockerPolicy)),
rego.Compiler(compiler),
rego.Input(commands),
)

// Evaluate the Rego query
rs, err := regoQuery.Eval(ctx)
if err != nil {
log.Fatal("Error evaluating query:", err)
}

t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Policy", "Status"})
var policyError error

for _, result := range rs {
if len(result.Expressions) > 0 {
keys := result.Expressions[0].Value.(map[string]interface{})
for key, value := range keys {
if value != true {
t.AppendRow(table.Row{key, color.New(color.FgRed).Sprint("failed")})
policyError = errors.New("policy evaluation failed: " + key)
} else {
t.AppendRow(table.Row{key, color.New(color.FgGreen).Sprint("passed")})
}
switch err := err.(type) {
case ast.Errors:
for _, e := range err {
fmt.Printf("code: %v", e.Code)
fmt.Printf("row: %v", e.Location.Row)
fmt.Printf("filename: %v", e.Location.File)
}
} else {
log.Error("No policies passed")
policyError = errors.New("no policies passed")
log.Fatal("Error evaluating query:", err)
}
}

t.Render()

if err != nil {
return errors.New("error evaluating Rego")
if err := PrintResults(rs); err != nil {
return fmt.Errorf("error evaluating rego results for %s: %v", regoPolicyPath, err)
}

return policyError
return nil
}
Loading
Loading