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

fix(rego): improve commands parsing #113

Merged
merged 4 commits into from
May 14, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
bundle.tar.gz
opa
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ rego: fmt-rego test-rego

.PHONY: fmt-rego
fmt-rego:
opa fmt -w checks/
opa fmt -w lib/ checks/

.PHONY: test-rego
test-rego:
Expand Down
4 changes: 2 additions & 2 deletions checks/docker/update_instruction_alone.rego
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ package_managers = {
deny[res] {
run := docker.run[_]
run_cmd := concat(" ", run.Value)
cmds := regex.split(`\s*&&\s*`, run_cmd)
cmds := sh.parse_commands(run_cmd)

some package_manager
update_indexes := has_update(cmds, package_managers[package_manager])
Expand All @@ -66,7 +66,7 @@ update_followed_by_install(cmds, package_manager, update_indexes) {

contains_cmd_with_package_manager(cmds, cmds_to_check, package_manager) = cmd_indexes {
cmd_indexes = [idx |
cmd_parts := split(cmds[idx], " ")
cmd_parts := cmds[idx]
some i, j
i != j
cmd_parts[i] == package_manager[_]
Expand Down
19 changes: 19 additions & 0 deletions checks/docker/update_instruction_alone_test.rego
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,25 @@ test_allowed {
count(r) == 0
}

test_allowed_cmds_separated_by_semicolon {
r := deny with input as {"Stages": [{"Name": "ubuntu:18.04", "Commands": [
{
"Cmd": "from",
"Value": ["ubuntu:18.04"],
},
{
"Cmd": "run",
"Value": ["apt-get update -y ; apt-get install -y curl"],
},
{
"Cmd": "entrypoint",
"Value": ["mysql"],
},
]}]}

count(r) == 0
}

test_allowed_multiple_install_cmds {
r := deny with input as {"Stages": [{"Name": "ubuntu:18.04", "Commands": [
{
Expand Down
2 changes: 2 additions & 0 deletions cmd/opa/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"os"

// register Built-in Functions from defsec
"github.com/aquasecurity/trivy-checks/pkg/rego"
_ "github.com/aquasecurity/trivy/pkg/iac/rego"
"github.com/open-policy-agent/opa/cmd"
)

func main() {
rego.RegisterBuiltins()
// runs: opa test lib/ checks/
if err := cmd.RootCommand.Execute(); err != nil {
fmt.Println(err)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.28.0
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/sh/v3 v3.8.0
)

require (
Expand Down
10 changes: 6 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0=
github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -627,8 +627,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
Expand Down Expand Up @@ -1319,6 +1319,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
mvdan.cc/sh/v3 v3.8.0 h1:ZxuJipLZwr/HLbASonmXtcvvC9HXY9d2lXZHnKGjFc8=
mvdan.cc/sh/v3 v3.8.0/go.mod h1:w04623xkgBVo7/IUK89E0g8hBykgEpN0vgOj3RJr6MY=
oras.land/oras-go/v2 v2.3.1 h1:lUC6q8RkeRReANEERLfH86iwGn55lbSWP20egdFHVec=
oras.land/oras-go/v2 v2.3.1/go.mod h1:5AQXVEu1X/FKp1F9DMOb5ZItZBOa0y5dha0yCm4NR9c=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
Expand Down
28 changes: 28 additions & 0 deletions lib/sh/sh_test.rego
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package lib.sh

test_parse_commands_with_ampersands {
cmds := sh.parse_commands("apt update && apt install curl")
count(cmds) == 2
cmds[0] == ["apt", "update"]
cmds[1] == ["apt", "install", "curl"]
}

test_parse_commands_empty_input {
cmds := sh.parse_commands("")
count(cmds) == 0
}

test_parse_commands_with_semicolon {
cmds := sh.parse_commands("apt update;apt install curl")
count(cmds) == 2
cmds[0] == ["apt", "update"]
cmds[1] == ["apt", "install", "curl"]
}

test_parse_commands_mixed {
cmds := sh.parse_commands("apt update; apt install curl && apt install git")
count(cmds) == 3
cmds[0] == ["apt", "update"]
cmds[1] == ["apt", "install", "curl"]
cmds[2] == ["apt", "install", "git"]
}
15 changes: 15 additions & 0 deletions pkg/rego/builtin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package rego

import (
"sync"

opa "github.com/open-policy-agent/opa/rego"
)

var registerOnce sync.Once

func RegisterBuiltins() {
registerOnce.Do(func() {
opa.RegisterBuiltin1(shParseCommandsDecl, shParseCommandsImpl)
})
}
73 changes: 73 additions & 0 deletions pkg/rego/parse_commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package rego

import (
"bytes"
"fmt"
"strings"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"github.com/open-policy-agent/opa/topdown/builtins"
"github.com/open-policy-agent/opa/types"
"mvdan.cc/sh/v3/syntax"
)

var shParseCommandsDecl = &rego.Function{
Name: "sh.parse_commands",
Decl: types.NewFunction(types.Args(types.S), types.NewArray(nil, types.NewArray(nil, types.S))),
Description: "Parse command sequence",
Memoize: true,
}

var shParseCommandsImpl = func(c rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {
astr, err := builtins.StringOperand(a.Value, 0)
if err != nil {
return nil, fmt.Errorf("invalid parameter type: %w", err)
}

commands, err := parseCommands(string(astr))

if err != nil {
return nil, fmt.Errorf("parse command sequence error: %w", err)
}

var commandsTerm []*ast.Term
for _, cmd := range commands {
var cmdTerm []*ast.Term
for _, cmd_part := range cmd {
cmdTerm = append(cmdTerm, ast.StringTerm(cmd_part))
}
commandsTerm = append(commandsTerm, ast.ArrayTerm(cmdTerm...))
}

return ast.ArrayTerm(commandsTerm...), nil
}

func parseCommands(cmdsSeq string) ([][]string, error) {
f, err := syntax.NewParser().Parse(strings.NewReader(cmdsSeq), "")
if err != nil {
return nil, err
}

printer := syntax.NewPrinter()

var commands [][]string
syntax.Walk(f, func(node syntax.Node) bool {
switch x := node.(type) {
case *syntax.CallExpr:
args := x.Args
var cmd []string
for _, word := range args {
var buffer bytes.Buffer
printer.Print(&buffer, word)
cmd = append(cmd, buffer.String())
}
if cmd != nil {
commands = append(commands, cmd)
}
}
return true
})

return commands, nil
}
40 changes: 40 additions & 0 deletions pkg/rego/parse_commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package rego

import (
"testing"

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

func TestParseCommands(t *testing.T) {
tests := []struct {
cmdsSeq string
expected [][]string
}{
{
cmdsSeq: "apt update; apt install -y nginx",
expected: [][]string{{"apt", "update"}, {"apt", "install", "-y", "nginx"}},
},
{
cmdsSeq: "apt update && apt install -y nginx",
expected: [][]string{{"apt", "update"}, {"apt", "install", "-y", "nginx"}},
},
{
cmdsSeq: "apt update || apt install -y nginx",
expected: [][]string{{"apt", "update"}, {"apt", "install", "-y", "nginx"}},
},
{
cmdsSeq: `echo "test;test" ;apt update && apt install -y nginx`,
expected: [][]string{{"echo", "\"test;test\""}, {"apt", "update"}, {"apt", "install", "-y", "nginx"}},
},
}

for _, test := range tests {
t.Run(test.cmdsSeq, func(t *testing.T) {
got, err := parseCommands(test.cmdsSeq)
require.NoError(t, err)
assert.Equal(t, test.expected, got)
})
}
}
6 changes: 6 additions & 0 deletions test/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import (
"github.com/liamg/memoryfs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

builtinrego "github.com/aquasecurity/trivy-checks/pkg/rego"
)

func init() {
builtinrego.RegisterBuiltins()
}

func getFileName(fpath string, info os.FileInfo, typePolicy bool) string {
pathParts := strings.Split(fpath, filepath.FromSlash("/"))
fileName := info.Name()
Expand Down